diff --git a/.claude/skills/hq-amend-design/SKILL.md b/.claude/skills/hq-amend-design/SKILL.md
index cbd24fcb..70cde27b 100644
--- a/.claude/skills/hq-amend-design/SKILL.md
+++ b/.claude/skills/hq-amend-design/SKILL.md
@@ -23,9 +23,10 @@ and says they disagree.
## Amending the to-be layer
-1. Write the decision record. If it reverses an earlier one, that record gets
- `status: superseded` and `superseded-by:` — its text is never edited.
-2. Edit the design; set `updated:` to today.
+1. Propose the decision record in the knowledge base (`hq-graduate`, steps 2 and 3). If it reverses an
+ earlier one, it `supersedes` it, or `changes` the clauses it replaces — the earlier record's text is
+ never edited.
+2. Edit the design, add `"ADR NNNN"` to its `decisions:`, and set `updated:` to today.
3. If this came from an issue, give the issue the document in the issue tracker: `amended-design` on its
`issue-tracker.move` to `resolved`, or `issue-tracker.link` with kind `design` before then. Never in the
report's frontmatter (ADR 0275).
diff --git a/.claude/skills/hq-defer/SKILL.md b/.claude/skills/hq-defer/SKILL.md
index 33e674a1..7cd6bd9f 100644
--- a/.claude/skills/hq-defer/SKILL.md
+++ b/.claude/skills/hq-defer/SKILL.md
@@ -18,7 +18,7 @@ Record it as **one memory file** in Claude's persistent memory directory for thi
path is given in the session's memory instructions), with `metadata.type: project`, plus a
one-line pointer in `MEMORY.md`.
-**Not** in `04-ISSUES`, `01-RESEARCH` or anywhere else in the repository:
+**Not** in `04-ISSUES`, the knowledge base, or anywhere else in the repository:
- A parked thought is not an issue or a research effort. Giving it a number asserts it has been
triaged, which is exactly what deferring says has not happened.
diff --git a/.claude/skills/hq-graduate/SKILL.md b/.claude/skills/hq-graduate/SKILL.md
index 30e8620e..4869be10 100644
--- a/.claude/skills/hq-graduate/SKILL.md
+++ b/.claude/skills/hq-graduate/SKILL.md
@@ -8,25 +8,34 @@ description: Use when a research effort in HQ concludes and becomes design, or w
Closes a research effort into a decision and a design, or amends an existing design.
**Authoritative playbook:**
[`00-META/process/02-graduation.md`](../../../00-META/process/02-graduation.md) — read
-it; this skill adds only the scaffolding.
+it; this skill adds only the scaffolding. Decision records and research efforts live whole in the
+knowledge base (seat `knowledge-base`, ADR 0290), reached through the mesh MCP server; nothing of them is
+written into this repository.
## Steps
1. **Check against GENESIS** — `mission.md`, `context.md`, `effect.md`. If the conclusion
conflicts, say which is wrong, in writing, before proceeding.
-2. **Write the decision record** in `02-DECISIONS/`, next free number, frontmatter per
- [`02-DECISIONS/README.md`](../../../02-DECISIONS/README.md). `reconstructed: false` — this one is being taken,
- not recovered. Record the rejected options.
-3. **Write the design** under `03-DESIGN/01-to-be/` with `layer: to-be`, `status: designed`,
- empty `code:`, today's `updated:`, and `decisions:` pointing at the record.
-4. **Close the effort** — the research `00-overview.md` gets `status: graduated` and `became:`
- pointing at both.
+2. **Reserve the record's number.** `mesh_call knowledge-base.reserve {"kind": "decision", "for": "
"}`.
+3. **Propose the decision record.** `mesh_call knowledge-base.propose` with the reserved `number`, `title`,
+ `topic` (one of the six in [`02-DECISIONS/README.md`](../../../02-DECISIONS/README.md)), `date`,
+ `deciders`, `clauses` (each `"**Lead.** the statement"`), `sections` (Context, Considered Options,
+ Consequences, How it is checked, References), `links` (`extends`, `supersedes`, `changes` with
+ `"NNNN §N"`, `cites`), `homes` (the design's path), and `needs-operator: true` when the operator must
+ approve it. A refusal names every rule broken: fix them all and propose again. The knowledge base
+ reviews it itself; on `changes` revise and propose again with `why`. `knowledge-base.get` shows its
+ status, reviews and the operator's ask.
+4. **Write the design** under `03-DESIGN/01-to-be/` with `layer: to-be`, `status: designed`,
+ empty `code:`, today's `updated:`, and `decisions:` listing `"ADR NNNN"`.
+5. **Close the effort** — `knowledge-base.propose` the effort again, whole, with `status: graduated` and
+ `became` links to the record (`"NNNN"`) and the design (its path).
## Amending an existing design
-A design changes only through a decision. Write the record first; if it reverses an earlier
-one, set that record's `status: superseded` and `superseded-by:` — **never edit its text**.
-Then edit the design and set `updated:`.
+A design changes only through a decision. Propose the record first; if it reverses an earlier one,
+it `supersedes` it (or `changes` the clauses it replaces) — **never edit the earlier record's text**;
+a correction of fact is a progressive insight (`propose` the accepted record with `insight`). Then edit
+the design, add `"ADR NNNN"` to its `decisions:`, and set `updated:`.
## When something ships
@@ -42,3 +51,4 @@ cannot be checked, it is `in-progress`.
- Do not move a to-be document into `00-as-is/`.
- Do not edit an as-is document to describe an intention.
- Do not change a decision record's meaning. Supersede it.
+- Do not write a decision record, or a copy of one, into this repository.
diff --git a/.claude/skills/hq-handoff/SKILL.md b/.claude/skills/hq-handoff/SKILL.md
index 35cbdcb0..73dc1796 100644
--- a/.claude/skills/hq-handoff/SKILL.md
+++ b/.claude/skills/hq-handoff/SKILL.md
@@ -10,8 +10,8 @@ Hands a settled design to a code repository. **Authoritative playbook:**
## Steps
-1. **Confirm it is settled.** `status: designed`, and every claim traceable to a record in
- `02-DECISIONS/`. An open question in the text is a reason to run `hq-new-research`, not to build
+1. **Confirm it is settled.** `status: designed`, and every claim traceable to a decision record in the knowledge base
+ (`knowledge-base.get` for each entry of its `decisions:`). An open question in the text is a reason to run `hq-new-research`, not to build
around it.
2. **Name the owner.** Set `code:` from
[`00-META/repos.md`](../../../00-META/repos.md). If the repository does not exist yet,
diff --git a/.claude/skills/hq-new-research/SKILL.md b/.claude/skills/hq-new-research/SKILL.md
index aacc423a..8816a874 100644
--- a/.claude/skills/hq-new-research/SKILL.md
+++ b/.claude/skills/hq-new-research/SKILL.md
@@ -5,38 +5,37 @@ description: Use when starting a new research effort in HQ — an idea, technolo
# hq-new-research
-Scaffolds a new research effort. **Authoritative playbook:**
+Starts a new research effort in the knowledge base. **Authoritative playbook:**
[`00-META/process/01-research.md`](../../../00-META/process/01-research.md) — read it;
-this skill only does the mechanical setup.
+this skill only does the mechanical setup. A research effort lives whole in the knowledge base (seat
+`knowledge-base`, ADR 0290); nothing of it is written into this repository.
## Steps
-1. Find the next free number: list `01-RESEARCH/NNN-*`, take the highest plus one, zero-padded
- to three digits.
-2. Create `01-RESEARCH/NNN-descriptive-name/` (kebab-case from the topic).
-3. Create `00-overview.md` in it with exactly this frontmatter, then a prose summary of **what** is
- being investigated, **why**, and **what it touches**:
+All through the mesh MCP server (`mesh_describe` gives each verb's arguments).
- ```yaml
- ---
- status: active
- initiated: YYYY-MM-DD
- touches: []
- became: []
- ---
- ```
-
-4. Do the research in further documents in the same folder. Keep the summary current.
+1. **Read what is known.** `mesh_call knowledge-base.ask {"question": ""}`;
+ read an effort or record it names with `knowledge-base.get` (`kind: research` for an effort,
+ `reasoning: true` for its documents).
+2. **Reserve the number.** `mesh_call knowledge-base.reserve {"kind": "research", "for": ""}`. The
+ number is spent; never pick one by hand.
+3. **Propose the effort.** `mesh_call knowledge-base.propose` with `kind: research`, the reserved `number`,
+ `title`, `status: active`, `date` (today), `sections` (the documents in order, each `name` and `text`;
+ the first `00-overview.md`: **what** is investigated, **why**, and **what it touches**), and `links`
+ of kind `cites` for every record, effort, issue or design it touches.
+4. **Do the research** in further documents of the same effort. Each change is `propose` again with the
+ effort whole and `why`. A refusal names every rule broken: fix them all and propose again. On review
+ `changes`, revise and propose again; `knowledge-base.get` shows the reviews.
## Do not
-- Do not restate the status in prose. It lives in frontmatter, in one place.
-- Do not fill `became:` while the effort is open — `hq-graduate` sets it at closure.
-- Do not skip or reuse a sequence number.
+- Do not restate the status in prose. It is the effort's `status`, in one place.
+- Do not give a `became` link while the effort is open — `hq-graduate` gives it at closure.
- Do not write into `03-DESIGN` from an open effort.
+- Do not write the effort, or a copy of it, into this repository.
- Do not name the mesh being observed. Evidence is required; identification is forbidden.
## Closing
An effort never just stops. It closes through `hq-graduate` as `graduated` or `abandoned`,
-always with `became:` pointing at what it turned into. Nothing is deleted.
+always with a `became` link to what it turned into. Nothing is deleted.
diff --git a/.claude/skills/hq-status/SKILL.md b/.claude/skills/hq-status/SKILL.md
index d165ed27..f91b6777 100644
--- a/.claude/skills/hq-status/SKILL.md
+++ b/.claude/skills/hq-status/SKILL.md
@@ -5,7 +5,7 @@ description: Use when you need a cross-cutting view of where HQ stands — resea
# hq-status
-Generates a cross-cutting view **from frontmatter**. This is a read-and-render skill, not a
+Generates a cross-cutting view **from frontmatter, the knowledge base and the issue tracker**. This is a read-and-render skill, not a
workflow — HQ has **no central status file by design** (decision 35). Every view is
generated on demand and never written back to disk.
@@ -13,29 +13,29 @@ generated on demand and never written back to disk.
| Section | Files | Frontmatter |
|---|---|---|
-| Research | `01-RESEARCH/NNN-*/00-overview.md` | `status` (`active` / `graduated` / `abandoned`), `initiated`, `touches`, `became` |
+| Research | the knowledge base, not a file: `knowledge-base.ask`, and `knowledge-base.get` with `kind: research` and a number | `status` (`active` / `graduated` / `abandoned`), `date`, links (`became`, `cites`) |
| Design | `03-DESIGN/**/*.md` (not READMEs) | `layer` (`as-is` / `to-be`), `status` (`designed` / `in-progress` / `implemented` / `abandoned`), `code`, `updated`, `decisions` |
-| Decisions | `02-DECISIONS/NNNN-*.md` | `status` (`proposed` / `accepted` / `superseded`), `date`, `deciders`, `reconstructed`, `superseded-by`, `extends` |
+| Decisions | the knowledge base, not a file: `knowledge-base.holds` with a topic (the clauses in force, with their records), `knowledge-base.get` with a number, `knowledge-base.links` | `status` (`proposed` / `accepted` / `superseded`), `topic`, `date`, `deciders`, each clause's status, links both ways, what changed it since |
| Issues | the issue tracker, not a file: `mesh_call issue-tracker.list` (optionally `status`, `owner`, `phrase`) and `issue-tracker.get` | `status` (`open` / `diagnosing` / `located` / `fixing` / `resolved` / `wontfix`), `worker` and `stale` while `diagnosing` or `fixing`, `opened`, `located-in`, `fixed-by`, `amended-design`, `record` (the folder here, when it has one) |
## Steps
-1. Read the frontmatter block from every file above — a grep across each tree is enough, no
- need to load bodies — and the issues from the tracker. An issue report carries no status;
- where an issue stands is only in the tracker (ADR 0275).
-2. Render what was asked as Markdown tables. Group and sort sensibly. The **ADR index** is one
- of these views, and it is the only place the list of records is shown
- ([ADR 0248](../../../02-DECISIONS/0248-the-decision-index-is-generated-when-it-is-read-and-never-stored.md)):
- run `python3 00-META/checks/index.py --print` for the records grouped by topic in the reading
- order `02-DECISIONS/README.md` writes, each with its status when not `accepted`. Asked for it by
- number instead, render records ordered by number, with title, date and status, and reconstructed
- ones marked. Off this checkout, the records module's `records_decisions` answers the same list.
+1. Read the designs' frontmatter (a grep across `03-DESIGN/` is enough, no need to load bodies), the
+ issues from the tracker, and the records from the knowledge base. An issue report carries no status;
+ where an issue stands is only in the tracker (ADR 0275). A decision record's or research effort's
+ status is only in the knowledge base (ADR 0290).
+2. Render what was asked as Markdown tables. Group and sort sensibly. The **ADR index** by topic is
+ `knowledge-base.holds` with each topic of the reading order `02-DECISIONS/README.md` writes, in that
+ order, with a `bound` large enough for the topic (it says what it cut); it is the only place the list is
+ shown (ADR 0248). The knowledge base has no verb that lists every record by number: a view by number
+ is built from `get` per number, and says which numbers it read.
3. Flag anything inconsistent at the end, as flags — do not silently correct the render:
- - a `graduated` or `abandoned` effort with an empty `became:`
+ - a `graduated` or `abandoned` effort with no `became` link
- an `implemented` design with an empty `code:`
- - a `superseded` record with no `superseded-by:`
+ - a `superseded` record nothing `supersedes`
- an `open` or `diagnosing` issue with no activity
- - a to-be design whose `decisions:` points at a record that does not exist
+ - a to-be design whose `decisions:` names a record `knowledge-base.get` does not find, or one that is
+ not `accepted`
- an as-is design whose claims are older than its `updated:` date suggests
## Do not
@@ -45,5 +45,5 @@ generated on demand and never written back to disk.
already drifted after a single addition, which is why it was removed, and the generated one
written into `02-DECISIONS/README.md` made every two open decisions conflict (issue 297), which
is why that went too. `index.py` fails if the list is stored there again.
-- Do not infer status from prose. Trust only the frontmatter, and the tracker for an issue; if
- either is wrong, flag it.
+- Do not infer status from prose. Trust only a design's frontmatter, the tracker for an issue and the
+ knowledge base for a record; if one is wrong, flag it.
diff --git a/.claude/skills/hq-sync-constitution/SKILL.md b/.claude/skills/hq-sync-constitution/SKILL.md
index c17b7f82..e02ad2f2 100644
--- a/.claude/skills/hq-sync-constitution/SKILL.md
+++ b/.claude/skills/hq-sync-constitution/SKILL.md
@@ -27,7 +27,8 @@ the claim "HQ is the source" is checked.
3. Publish it to the knowledge base under the constitution slug, replacing the body.
4. **Read it back and verify the change is present.** A publish that reported success and did
nothing is exactly the failure class this repository exists to name — do not skip this.
-5. Note the sync in the decision record's Consequences.
+5. Note the sync in the decision record's Consequences (in the knowledge base: `knowledge-base.propose`
+ the proposed record again with the note and `why`).
## Do not
diff --git a/00-META/README.md b/00-META/README.md
index d3c6021d..386cb6b7 100644
--- a/00-META/README.md
+++ b/00-META/README.md
@@ -35,7 +35,7 @@ comparing it against the code rather than by anyone noticing:
- It described the pipeline as having a separate builder process and a build stage that
packages. Neither was true after 2026-08-04; the documents stayed stale until 2026-08-06
- ([ADR 0010](../02-DECISIONS/0010-delivery.md)).
+ (ADR 0010).
- It listed the mesh as spanning a fixed number of named machines, which is exactly the
content this repository cannot carry.
@@ -45,10 +45,10 @@ symlinks at all — the rule is not merely "only the installer may link", and a
elevating linking to a principle points the opposite way from where this is going.
What exists today is that the installer owns and reconciles every link
-([ADR 0012](../02-DECISIONS/0012-the-mesh-creates-no-symlinks.md)) — an as-is fact, recorded in
+(ADR 0012) — an as-is fact, recorded in
[`03-DESIGN/00-as-is/05-runtime-and-installation.md`](../03-DESIGN/00-as-is/05-runtime-and-installation.md).
Centralising who may link narrowed the incident class; it did not close it. The intent is to
-remove the mechanism, recorded as [ADR 0012](../02-DECISIONS/0012-the-mesh-creates-no-symlinks.md).
+remove the mechanism, recorded as ADR 0012.
A founding document contradicting the direction of travel is precisely the failure this folder
exists to prevent.
diff --git a/00-META/checks/README.md b/00-META/checks/README.md
index 81cb3f45..51a58718 100644
--- a/00-META/checks/README.md
+++ b/00-META/checks/README.md
@@ -1,9 +1,9 @@
# Checks
```
-python3 00-META/checks/records.py structure: links, citations, supersession, topics
-python3 00-META/checks/index.py every record's topic is in the reading order, and no list of records is stored
-python3 00-META/checks/index.py --print the records, by topic, in reading order (generated, never written)
+python3 00-META/checks/records.py structure: links, no record written here, citations in the form "ADR NNNN"
+python3 00-META/checks/index.py the reading order's topics are written, and no list of records is stored
+python3 00-META/checks/cycle.py the development cycle: a design's layer, status, decisions and code; an issue report's frontmatter
python3 00-META/checks/words.py the glossary's retired words are not used, no daemon's line is written, and no word is defined twice
python3 00-META/checks/words.py --list tools the words the catalogue's copy must list
python3 00-META/checks/words.py --list lines the daemons' lines the catalogue's copy must list
@@ -23,31 +23,49 @@ indistinguishable from one that cannot.
| Check | Asserts | Found |
|---|---|---|
| `links` | every relative link resolves | — (run ad hoc during authoring; now permanent) |
-| `rests-on` | `decisions:` and `extends:` name records that exist and are **accepted** | the class behind both incidents |
-| `live-citation` | a governing document citing a **superseded** record names its replacement in the same paragraph | `01-to-be/README.md` citing ADR 0022 as live guidance |
-| `supersession` | if A says it was superseded by B, B says it supersedes A | ADR 0012 never declared that it superseded 0011 |
-| `partial-supersession` | a record's `supersedes-in-part:` names records that exist, each carrying a dated *Superseded in part* note naming it under its heading; and every such note is named back by its superseder's field | 11 of 21 partial supersessions had no note on the record they change, and 6 more were said only in prose ([research 045](../../01-RESEARCH/045-a-knowledge-base-the-mesh-writes/00-overview.md)) |
-| `clauses` | an accepted record dated from 2026-10-11 writes its `## Decision` section as numbered clauses with a bold lead ([ADR 0290](../../02-DECISIONS/0290-a-decision-record-lives-whole-in-the-knowledge-base-made-of-clauses.md) rule 3) | 226 of 263 decision records wrote their decision as prose, so the knowledge base must draft their clauses |
-| `numbering` | the number in the filename is the number in the heading | — |
-| `topics` | every record names a topic of the reading order in `02-DECISIONS/README.md` | — |
-| *(index.py)* | the reading order is written, every record's topic is one of it, and the README stores no list of records ([ADR 0248](../../02-DECISIONS/0248-the-decision-index-is-generated-when-it-is-read-and-never-stored.md)) | a stored list made every two open decisions conflict ([issue 297](../../04-ISSUES/297-two-open-decisions-always-conflict/00-report.md)); it failed on the README of `main` before the list was removed |
+| `no-records` | `02-DECISIONS/` and `01-RESEARCH/` hold their README and nothing else: a decision record or research effort lives in the knowledge base (ADR 0290 rule 9) | written with the change that removed the records (issue 426); it failed on a record file put back in `02-DECISIONS/` before it passed |
+| `decisions-form` | every entry of a design's `decisions:` is "ADR NNNN", four digits | written with issue 426; it failed on an entry `ADR 290` before it passed |
+| `mention-form` | every "ADR N" in the text has four digits, and every "research N" three; a code span is an example and is not read | written with issue 426; it failed on `ADR 12` and `research 45` before it passed |
+| *(index.py)* | the reading order's six topics are written in `02-DECISIONS/README.md`, none twice, and the README stores no list of records (ADR 0248). The knowledge base reads these lines and refuses a record whose topic is not one of them | a stored list made every two open decisions conflict ([issue 297](../../04-ISSUES/297-two-open-decisions-always-conflict/00-report.md)); it failed on the README of `main` before the list was removed |
| `status-vs-code` | a to-be document naming specific code is not still `designed` | **ten documents**, several with a *What was built* section, describing lab-proven code |
+## Where the rules on records went
+
+The decision records and research efforts left this repository for the knowledge base (ADR 0290, issue 426),
+and the rules these checks held over them left with them. The knowledge base holds each as a rule of its own,
+named in its refusal: `knowledge-base.propose` refuses a record that breaks one of the rules that judge one
+record, naming every reason at once, and the four rules that judge the whole record set run after every
+acceptance and raise a condition when one fails (mesh-knowledge's `rules.go`, one table-test case per rule).
+
+| Rule here until issue 426 | Where it lives now |
+|---|---|
+| `records.py` `rests-on`, for a record's `extends:` | the knowledge base's `rests-on` at propose, and the whole-set rule `rests-on-live` |
+| `records.py` `rests-on`, for a design's `decisions:` naming an accepted record | **nowhere yet**: this repository cannot read the knowledge base. The reviewer checks each new entry with `knowledge-base.get`. Designs become the knowledge base's own kind under issue 428 |
+| `records.py` `live-citation`, in a record | the knowledge base's `live-citation` |
+| `records.py` `live-citation`, in `00-META/` and `03-DESIGN/01-to-be/` | **nowhere yet**, as above: the reviewer checks a cited record's status with `knowledge-base.get`; issue 428 |
+| `records.py` `supersession` | the knowledge base's `supersedes-accepted` and `self-link`, and the whole-set rule `supersession` |
+| `records.py` `partial-supersession` | the knowledge base's `changes` link to a clause, `changes-in-force` and `supersedes-accepted`, and the whole-set rule `partial` |
+| `records.py` `clauses` | the knowledge base's `has-clause`, `clause-numbering`, `clause-lead` and `clause-size` |
+| `records.py` `numbering` | the knowledge base's `title` and `reserved` |
+| `records.py` `topics`, `index.py` (a record's topic) | the knowledge base's `topic`, against the reading order `index.py` still checks |
+| `records.py` `insights` | the knowledge base's `insight-form`; an accepted record changes by a new version marked as a progressive insight |
+| `index.py --print` | `knowledge-base.holds` with a topic |
+| `cycle.py` research: a known status, `became:` when graduated | the knowledge base's `research-status`, `overview` and `became` |
+| `cycle.py` no two decision records share a number | the knowledge base's `reserved`, and the issue tracker's counter behind `knowledge-base.reserve` |
+| `cycle.py` an accepted decision cited by nothing | the knowledge base's whole-set rule `cited`: a record accepted there is cited by another record or names a home (`homes` at propose) |
+| `cycle.py` a deferral names the issue that tracks it | the knowledge base's `deferral` |
+| `words.py` over research efforts from 2026-10-07, and `kept` in `words-allowed.md` | the knowledge base's `retired-word` and `daemon-line`, from this glossary |
+
## What is deliberately not checked
-- **`02-DECISIONS/` and `01-RESEARCH/` may cite superseded records freely.** A decision record
- discusses history; research records what was observed. Flagging those would produce noise on
- correct documents, and a check that cries wolf gets suppressed — which costs more than not
- having it.
-- **`03-DESIGN/00-as-is/` may rest on a superseded record.** It describes what runs, and what
- runs was built under whatever was decided at the time
- ([ADR 0006](../../02-DECISIONS/0006-the-substrate-and-the-control-plane.md):
- *as-is describing a superseded decision is exactly what as-is is for*).
-- **Whether a citation's prose is still true.** Only whether the record it points at is live.
- A document can cite an accepted record and describe it wrongly, and nothing here notices.
-
-So "governing" means `00-META/` and `03-DESIGN/01-to-be/` — the documents that tell somebody
-what to do.
+- **Whether a record a document cites exists, or is still accepted.** A checkout cannot read the knowledge
+ base, and a check that called the mesh would make every merge here depend on it. Until designs become the
+ knowledge base's own kind (issue 428), the reviewer asks `knowledge-base.get` for each record a change
+ cites in `00-META/` or `03-DESIGN/01-to-be/` (the documents that tell somebody what to do). An as-is
+ document may rest on a superseded record: it describes what runs, and what runs was built under whatever
+ was decided at the time (ADR 0006: *as-is describing a superseded decision is exactly what as-is is for*).
+- **Whether a citation's prose is still true.** A document can cite an accepted record and describe it
+ wrongly, and nothing here notices.
## Adding a check
@@ -56,51 +74,35 @@ whose failure has never been observed is a guess about its own correctness.
## cycle.py
-The development cycle, checked ([ADR 0080](../../02-DECISIONS/0080-the-development-cycle-is-checked.md)):
-a to-be design names a decision, an in-progress/implemented design names its owning code, a graduated
-research overview says what it became, and no two issue records share a number (issue 155 — the
-number is how a record is cited, and `main` lags every open pull request, so two people reading it
-allocate the same one). An issue report carries the day it was `opened:` and no status key — `status:`,
-`located-in:`, `fixed-by:`, `amended-design:`, `replay:`, `replay-none:` or `resolved:` — because an issue's
-status lives in the issue tracker ([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));
-it failed on all 361 reports that carried them before they were removed. The rules on an issue's status this
-check held until then — an owner for a located issue, a fix for a resolved one, and a core issue's `replay`
-or `replay-none` ([ADR 0237](../../02-DECISIONS/0237-a-change-is-judged-against-the-mesh-that-runs-before-it-merges-on-the-build-seat.md))
-— are refused at the write by `issue-tracker.move`. And an accepted record that puts work
-off — "until renamed", "left for later", "not decided here", "postponed", "future work", "deferred", "a later
-change" — names the issue that tracks it, as tracking it ("tracked by issue N", or "issue N tracks"), in that
-paragraph or list item, or in a dated progressive insight that speaks about it: one in the same section, or one
-that quotes it. An issue cited for another reason tracks nothing, and one insight does not excuse every deferral
-in a record. The tracking issue counts when it has a folder in `04-ISSUES/`, or when its number is above every
-folder's: since ADR 0275 an issue from 365 on lives only in the issue tracker, which a checkout cannot read, and the
-tracker hands out numbers in order (issue 436). A made-up number above the folders therefore passes; the reviewer
-checks that the issue exists, with `issue-tracker.get`. A rejected option is not work put off and is not held, judged by its `##` section, so an option under a
-`###` subheading of "Considered Options" is skipped too
-([issue 354](../../04-ISSUES/354-a-decided-rename-was-never-scheduled-and-the-operator-believed-it-live/00-report.md):
-ADR 0244 deferred the code renames with no issue, and two days later the operator believed them done). Held to
-it: a record dated from 2026-10-09, and an older one corrected under the rule. Measured from ADR 0244's date
-it found deferrals without an issue in ADR 0241, 0244 and 0270; held from 2026-10-09 it failed on 0270 and 0273
-(whose "not decided here" sits beside a citation of issue 340 that tracks none of it), and on 0244's four
-deferral places once its insight brought it under the rule — on three of them while the insight, in §6, named
-the issue without quoting them — before each named its issue (355, 357, 354). `python3 00-META/checks/cycle.py`
+The development cycle, checked (ADR 0080):
+every design parses, its `layer:` matches its folder and its `status:` is known; a to-be design names a
+decision in `decisions:`; an in-progress or implemented design names its owning code; and no two issue
+records share a number (issue 155 — the number is how a record is cited, and `main` lags every open pull
+request, so two people reading it allocate the same one). An issue report carries the day it was `opened:`
+and no status key — `status:`, `located-in:`, `fixed-by:`, `amended-design:`, `replay:`, `replay-none:` or
+`resolved:` — because an issue's status lives in the issue tracker (ADR 0275); it failed on all 361 reports
+that carried them before they were removed. The rules on an issue's status this check held until then — an
+owner for a located issue, a fix for a resolved one, and a core issue's `replay` or `replay-none` (ADR 0237)
+— are refused at the write by `issue-tracker.move`. Its rules on research efforts and decision records left
+with them (the table above). `python3 00-META/checks/cycle.py`
## words.py
-One word per thing, checked ([ADR 0244](../../02-DECISIONS/0244-the-mesh-is-described-in-domains-and-one-word-names-one-thing.md)).
+One word per thing, checked (ADR 0244).
It reads the glossary's *Not:* lines (the retired words, each with its scope) and *Identifier until
renamed* lines, and fails on a retired word or a bare identifier in running prose — what is left once code,
quotations, struck-through text, link targets, comments and frontmatter are taken out — in `00-META/`,
-`03-DESIGN/`, `AGENTS.md`, `README.md`, and research and issues dated from 2026-10-07. Decision records are
-never checked. It also fails when a glossary head word heads two entries or is also retired.
-[`words-allowed.md`](words-allowed.md) names a document that could not be reworded at once, with a date, or
-a graduated research effort kept as written. With `MESH_CATALOG_DIR` set it compares the catalogue's copy
+`03-DESIGN/`, `AGENTS.md`, `README.md`, and issues opened from 2026-10-07. Decision records and research
+efforts live in the knowledge base, which holds a proposed record to the same words (the table above). It
+also fails when a glossary head word heads two entries or is also retired.
+[`words-allowed.md`](words-allowed.md) names a document that could not be reworded at once, with a date. With `MESH_CATALOG_DIR` set it compares the catalogue's copy
of the tools' retired words (`retired-words`) with the glossary.
-It also fails on a **daemon's line** ([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),
+It also fails on a **daemon's line** (ADR 0272,
its insight of 2026-10-09): the operator types one command, `nox`, and a document names a `nox` line, never the
controller's or the node-engine's binary followed by one of its commands. The lines are the check's own list
(`DAEMON_LINES`); a command that is a line only with an argument after it is matched as a line is written. Looked
-for in the same documents, from 2026-10-09 for research and issues, in the document's own words — code blocks and
+for in the same documents, from 2026-10-09 for issues, in the document's own words — code blocks and
spans included, quotations and block quotes taken out. With `MESH_CATALOG_DIR` set it compares the catalogue's copy
(`retired-lines`) with the list.
diff --git a/00-META/checks/cycle.py b/00-META/checks/cycle.py
index 8fbd9835..c47ec031 100644
--- a/00-META/checks/cycle.py
+++ b/00-META/checks/cycle.py
@@ -22,12 +22,12 @@ What is enforced:
them, and two places holding one fact drift. The keys the reports opened before
the import carried were removed once the import held them; the commit it read
is on every imported issue.
- research a known `status:`; a `graduated` overview says what it `became:`, and every
- target it names exists.
- decisions every accepted record is REACHABLE from the cycle: cited by a design doc's
- frontmatter, a research overview, an issue report, a 00-META document, or another
- record's extends/supersedes chain. A decision nothing points at is one nobody will
- find by following pointers -- which is how records go stale in people's heads.
+
+The decision records and research efforts live in the knowledge base (ADR 0290, issue 426), and the
+rules this check held over them are the knowledge base's now: a research effort's status and what it
+became, and a decision's duplicate number, are refused by `knowledge-base.propose`; an accepted decision
+cited by nothing is the knowledge base's whole-set rule `cited`; a deferral that names no issue is its
+rule `deferral`. README.md beside this file says where each went.
Deliberately NOT enforced: as-is docs need no decisions (they describe what exists, not what
was decided).
@@ -43,89 +43,12 @@ import sys
ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", ".."))
DESIGN_STATUSES = {"proposed", "designed", "in-progress", "implemented", "abandoned"}
-RESEARCH_STATUSES = {"active", "graduated", "abandoned"}
# The keys an issue report carried while its status lived here. The issue tracker holds them now (ADR 0275,
# slice 3 of to-be 53), and `issue-tracker.move` enforces the cycle's rules on them at the write. A report
# carrying one of them is a status written in two places, whenever it was opened.
TRACKER_KEYS = ("status", "located-in", "fixed-by", "amended-design", "replay", "replay-none", "resolved")
-# A decision that defers work names the issue that tracks it (issue 354). ADR 0244 left the code
-# renames of the node-engine and the tool runner "for a later rename", with no issue, owner or date;
-# two days later the operator believed them done, and nothing in the record said otherwise. A deferral
-# nothing tracks is indistinguishable from work that was done. Held to it: an accepted record dated
-# from DEFERRALS_FROM, and an older one corrected under the rule (a progressive insight dated from then
-# on): an edit made under the rule is held to it. The phrases are the ones records use to put work off;
-# "for later" alone is not one, because records say "never queued for later" and "too narrow for later".
-# The tracking issue is named as tracking it, beside the deferral or in a dated insight in the same section
-# or quoting it (ISSUE_REF, covers).
-DEFERRALS_FROM = "2026-10-09"
-DEFERRAL = re.compile(r"(?i)until (?:it is |they are |the code (?:is )?)?renamed|until the code rename|"
- r"\ba later rename|not renamed here|identifier to rename|"
- r"left (?:for|to) (?:a )?later|not decided here|\bpostponed\b|future work|"
- r"\bdeferred\b|later work|not built here|\ba later change")
-# The issue that tracks a deferral is named as tracking it -- "tracked by issue N", or "issue N tracks" --
-# because a record cites issues for many reasons, and an issue cited as context beside a deferral (ADR 0273
-# names issue 340 in the very sentence that leaves a channel undecided) tracks nothing.
-ISSUE_REF = re.compile(r"(?i)tracked (?:by|in|as)\s+\[?issue (\d{3})|\[?issue (\d{3})\]?(?:\([^)]*\))?\s+tracks\b")
-INSIGHT = re.compile(r"\*\*Progressive insights?[ \t]*[—–-][ \t]*(\d{4}-\d{2}-\d{2})")
-
-
-def paragraphs(path):
- """A record's body as (first line number, section heading, chapter, text) paragraphs: frontmatter and fenced
- code left out, a block quote's markers stripped, so a quoted insight reads as the paragraph it is. The
- section is the nearest heading above the paragraph; a heading is its own paragraph and its own section.
- The chapter is the nearest `#` or `##` heading, which a `###` subheading inside it does not change."""
- text = open(path, encoding="utf-8").read()
- m = re.match(r"^---\n.*?\n---\n?", text, re.S)
- body, first = (text[m.end():], text[: m.end()].count("\n") + 1) if m else (text, 1)
- body = re.sub(r"(?ms)^\s*```.*?^\s*```", lambda x: re.sub(r"[^\n]", " ", x.group(0)), body)
- out, line, section, chapter = [], first, "", ""
- for chunk in body.split("\n\n"):
- stripped = "\n".join(re.sub(r"^\s*>\s?", "", l) for l in chunk.split("\n")).strip()
- if stripped:
- heading = re.match(r"^#{1,6}\s+(.*)", stripped)
- if heading:
- section = heading.group(1).strip()
- if len(stripped) - len(stripped.lstrip("#")) <= 2:
- chapter = section
- # A list's top-level items are units of their own: a Consequences list is one paragraph, and an
- # issue named in one item tracks nothing in the next.
- items = re.split(r"\n(?=[-*] |\d+\. )", stripped)
- at = line + chunk[: len(chunk) - len(chunk.lstrip())].count("\n")
- for item in items:
- out.append((at, section, chapter, item))
- at += item.count("\n") + 1
- line += chunk.count("\n") + 2
- return out
-
-
-QUOTE = re.compile(r"\"([^\"]{12,})\"|\u201c([^\u201d]{12,})\u201d")
-
-
-def flat(text):
- """Text compared for a quotation: markup and case dropped, whitespace collapsed."""
- return re.sub(r"\s+", " ", re.sub(r"[*`_]", "", text)).strip().lower()
-
-
-def covers(insight_section, insight_text, section, text):
- """An insight speaks about a deferral paragraph when it stands in the same section, or quotes it."""
- if insight_section == section:
- return True
- body = flat(text)
- return any(flat(a or b) in body for a, b in QUOTE.findall(insight_text))
-
-
-def issue_refs(text, issues):
- """The issues a text names as tracking it: one with a folder here, or one numbered above every folder.
-
- Since ADR 0275 an issue from 365 on lives only in the issue tracker, and ADR 0291 removes the folders; a
- checkout alone cannot see those, so a number above the highest folder counts as one the tracker holds
- (issue 436). The tracker hands out numbers in order, so such a number was issued after the last folder.
- """
- top = max((int(k) for k in issues if k.isdigit()), default=0)
- return {n for a, b in ISSUE_REF.findall(text) for n in (a, b) if n and (n in issues or int(n) > top)}
-
def rel(path):
return os.path.relpath(path, ROOT)
@@ -216,22 +139,6 @@ def main():
else:
seen[number] = name
- # And decision records, which 155's fix left out: on 2026-10-02 two ADRs numbered 0169 landed
- # on main from two sessions within the hour, and every check passed.
- seen_records = {}
- for path in sorted(glob.glob(os.path.join(ROOT, "02-DECISIONS", "[0-9]*.md"))):
- name = os.path.basename(path)
- number = name.split("-", 1)[0]
- if not number.isdigit():
- continue
- if number in seen_records:
- bad(os.path.join("02-DECISIONS", name),
- "is numbered %s, and so is %s -- a record's number is how it is cited. Take the next "
- "free number across main AND every open pull request; the branch that lands last "
- "renumbers" % (number, seen_records[number]))
- else:
- seen_records[number] = name
-
# Since the import of slice 3 (ADR 0275), an issue's status, owner, fix and replay live in the issue
# tracker, and its `move` refuses at the write what this check refused at merge. A report carries none
# of those keys.
@@ -250,91 +157,9 @@ def main():
"(ADR 0275): move it with `issue-tracker.move`, and keep no copy here"
% ", ".join("`%s:`" % key for key in carried))
- # ---- research ----------------------------------------------------------------------
- for path in sorted(glob.glob(os.path.join(ROOT, "01-RESEARCH", "*", "00-overview.md"))):
- front = frontmatter(path)
- if front is None:
- bad(path, "no frontmatter")
- continue
- status = front.get("status")
- if status not in RESEARCH_STATUSES:
- bad(path, "status %r is not one of %s" % (status, sorted(RESEARCH_STATUSES)))
- if status == "graduated":
- became = listy(front, "became")
- if not became:
- bad(path, "graduated but became: names nothing")
- for target in became:
- if not os.path.exists(os.path.join(ROOT, target)):
- bad(path, "became names %s, which does not exist" % target)
-
- # ---- decisions -------------------------------------------------------------------
- records = {}
- for path in sorted(glob.glob(os.path.join(ROOT, "02-DECISIONS", "[0-9]*.md"))):
- front = frontmatter(path)
- records[os.path.basename(path)] = (path, (front or {}).get("status"))
-
- cited = set()
- sources = (glob.glob(os.path.join(ROOT, "03-DESIGN", "*", "*.md"))
- + glob.glob(os.path.join(ROOT, "01-RESEARCH", "*", "00-overview.md"))
- + glob.glob(os.path.join(ROOT, "04-ISSUES", "*", "00-report.md"))
- + glob.glob(os.path.join(ROOT, "00-META", "**", "*.md"), recursive=True))
- for path in sources:
- text = open(path, encoding="utf-8").read()
- if os.sep + "03-DESIGN" + os.sep in path:
- # A design doc's governing citations live in frontmatter; a prose mention is
- # commentary, not a home.
- m = re.match(r"^---\n(.*?)\n---", text, re.S)
- text = m.group(1) if m else ""
- for m in re.finditer(r"([0-9]{4}-[^\s\)\],#]+\.md)", text):
- cited.add(os.path.basename(m.group(1)))
- for name in records:
- front = frontmatter(records[name][0]) or {}
- for key in ("extends", "supersedes", "superseded-by"):
- v = front.get(key)
- if isinstance(v, str) and v:
- cited.add(os.path.basename(v))
- for name, (path, status) in records.items():
- if status == "accepted" and name not in cited:
- bad(path, "an accepted decision nothing in the cycle cites -- give it a home in a "
- "design doc's decisions:, a research became:, an issue, or 00-META")
-
- # ---- deferrals -------------------------------------------------------------------
- # A paragraph or list item that puts work off names the issue that tracks it, or a dated
- # progressive insight that speaks about it (its section, or a quotation of it) does. An issue that has
- # since resolved still counts: the deferred work landed, which is what the pointer was for.
- for name, (path, status) in records.items():
- front = frontmatter(path) or {}
- decided = str(front.get("date") or "")
- if status != "accepted":
- continue
- paras = paragraphs(path)
- insights = [(d, sec, text) for line, sec, _, text in paras for d in INSIGHT.findall(text)]
- held = decided >= DEFERRALS_FROM or any(d >= DEFERRALS_FROM for d, _, _ in insights)
- if not held:
- continue
- # A dated insight naming an issue covers the deferrals it speaks about -- those in its own section,
- # or ones it quotes -- and no other: one insight must not excuse every deferral in the record.
- tracking = [(sec, text) for d, sec, text in insights if issue_refs(text, seen)]
- for line, section, chapter, text in paras:
- m = DEFERRAL.search(text)
- # A rejected option is a decision not to do it, not work put off -- judged by its chapter, so an
- # option under a `###` subheading of "## Considered Options" is skipped as well.
- if re.match(r"(?i)(options? rejected|considered options|rejected)", chapter):
- continue
- if not m or issue_refs(text, seen):
- continue
- if any(covers(isec, itext, section, text) for isec, itext in tracking):
- continue
- bad(path, "line %d defers work (%r) and names no issue that tracks it -- a deferral nothing "
- "tracks reads as done (issue 354). Name the issue beside it, with its owner, or in a "
- "dated progressive insight that says what it tracks"
- % (line + text.count("\n", 0, m.start()), m.group(0)))
-
checked = (
len(glob.glob(os.path.join(ROOT, "03-DESIGN", "0*", "*.md")))
+ len(glob.glob(os.path.join(ROOT, "04-ISSUES", "*", "00-report.md")))
- + len(glob.glob(os.path.join(ROOT, "01-RESEARCH", "*", "00-overview.md")))
- + len(records)
)
if failures:
print("cycle: %d document(s) break the development cycle:" % len(failures))
diff --git a/00-META/checks/index.py b/00-META/checks/index.py
index 2b7bb586..9750c537 100644
--- a/00-META/checks/index.py
+++ b/00-META/checks/index.py
@@ -1,23 +1,21 @@
#!/usr/bin/env python3
-"""The reading order of the decision records: its topics are written, its list is generated.
+"""The reading order of the decision records: its topics are written here, its records are not.
-A number identifies a record and never changes, so the folder listing is creation order rather
-than reading order. The reading order is the six topics `02-DECISIONS/README.md` writes down, in
-order, and each record's own `topic:`. The list of records under each topic is generated from
-that frontmatter when somebody reads it, and never stored (ADR 0248, hq issue 297): a stored
-list was a line every decision pull request added to one file, so any two open at once conflicted.
+A number identifies a record and never changes, so the reading order is not the order of the numbers.
+It is the six topics `02-DECISIONS/README.md` writes down, in order, and each record's own topic. The
+records live in the knowledge base (ADR 0290, issue 426), which reads these lines through the records
+module and refuses a record whose topic is not one of them; the list of records under a topic is its
+answer to `knowledge-base.holds` with that topic, never stored here (ADR 0248, hq issue 297).
python3 00-META/checks/index.py check
- python3 00-META/checks/index.py --print print the list, by topic, in reading order
-It fails when the README's reading order is missing, when a record's topic is not one of its
-topics, and when the README stores a list of records again — the index markers, or a line of
-the generated list. The last one is what keeps the conflict from coming back.
+It fails when the README's reading order is missing or names a topic twice, and when the README stores
+a list of records again — the index markers, or a line of a generated list. The last one is what keeps
+the conflict of issue 297 from coming back. Whether every record's topic is one of the six is the
+knowledge base's rule `topic`, judged when a record is proposed.
"""
-import glob
import io
-import os
import re
import sys
@@ -25,13 +23,14 @@ README = "02-DECISIONS/README.md"
# A topic in the README's reading order, e.g.
# 1. **What the mesh is** — `topic: the mesh`. What it is for, ...
-# The records module's `records_decisions` reads the same line, so the form is a contract.
+# The knowledge base (mesh-knowledge, reference.go) and the records module's `records_decisions` read the
+# same line, so the form is a contract.
TOPIC_LINE = re.compile(r"^\d+\.\s+\*\*(?P[^*]+)\*\*\s+—\s+`topic:\s*(?P[^`]+)`", re.M)
-# What a stored list looks like: the old markers, or a line of the list this script prints.
+# What a stored list looks like: the old markers, or a line of the list index.py used to print.
STORED = [
(re.compile(r""), "an index marker"),
- (re.compile(r"^- \*\*\d{4}\*\* — \[", re.M), "a line of the generated list"),
+ (re.compile(r"^- \*\*\d{4}\*\* — \[", re.M), "a line of a generated list"),
]
@@ -42,72 +41,33 @@ def reading_order(text=None):
return [(m.group("topic").strip(), m.group("label").strip()) for m in TOPIC_LINE.finditer(text)]
-def field(text, name):
- m = re.search(r'^%s:\s*(.+)$' % name, text, re.M)
- return m.group(1).strip() if m else None
-
-
-def records():
- out = []
- for path in sorted(glob.glob('02-DECISIONS/0*.md')):
- text = io.open(path, encoding='utf-8').read()
- heading = re.search(r'^# \d+\.\s*(.+)$', text, re.M)
- out.append({
- "file": os.path.basename(path),
- "number": os.path.basename(path)[:4],
- "title": heading.group(1).strip() if heading else "(no heading)",
- "topic": field(text, "topic"),
- "status": field(text, "status"),
- })
- return out
-
-
-def render(topics, rs):
- lines = []
- for topic, label in topics:
- rows = [r for r in rs if r["topic"] == topic]
- if not rows:
- continue
- lines.append("### %s" % label)
- lines.append("")
- for r in rows:
- mark = "" if r["status"] == "accepted" else " *(%s)*" % r["status"]
- lines.append("- **%s** — [%s](%s)%s" % (r["number"], r["title"], r["file"], mark))
- lines.append("")
- return "\n".join(lines)
-
-
def main():
+ if "--print" in sys.argv:
+ print("index: the records live in the knowledge base (ADR 0290). Read a topic's with "
+ "`knowledge-base.holds` and that topic, through the mesh MCP server.")
+ return 2
+
text = io.open(README, encoding='utf-8').read()
topics = reading_order(text)
- rs = records()
-
- if "--print" in sys.argv:
- print(render(topics, rs))
- return 0
problems = []
if not topics:
problems.append("%s writes no reading order: no line of the form "
- "'1. **Label** — `topic: name`. ...'" % README)
+ "'1. **Label** — `topic: name`. ...'; the knowledge base refuses every "
+ "record without it" % README)
names = [t for t, _ in topics]
if len(set(names)) != len(names):
problems.append("%s names a topic twice in its reading order" % README)
for pattern, what in STORED:
if pattern.search(text):
- problems.append("%s stores the list of records (%s). The list is generated when read "
- "(ADR 0248): `python3 00-META/checks/index.py --print`" % (README, what))
- known = set(names)
- for r in rs if known else []: # with no reading order, every record would be named; one line says why
- if r["topic"] not in known:
- problems.append("%s: topic %r is not one of the reading order's: %s"
- % (r["file"], r["topic"], ", ".join(names)))
+ problems.append("%s stores a list of records (%s). The records live in the knowledge base, "
+ "and a topic's list is `knowledge-base.holds` with that topic" % (README, what))
for p in problems:
print("index: " + p)
if problems:
return 1
- print("index: %d topics, %d records, no stored list" % (len(topics), len(rs)))
+ print("index: %d topics in the reading order, no stored list" % len(topics))
return 0
diff --git a/00-META/checks/records.py b/00-META/checks/records.py
index 2d9c3b64..9b5415d9 100644
--- a/00-META/checks/records.py
+++ b/00-META/checks/records.py
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
-"""Structural checks over HQ's own records.
+"""Structural checks over HQ's own documents.
Every check here exists because the thing it checks for actually happened. See README.md
for which incident is behind which check. Run from the repository root:
@@ -7,6 +7,11 @@ for which incident is behind which check. Run from the repository root:
python3 00-META/checks/records.py
Exits non-zero if anything fails, so it can be a gate rather than a report.
+
+The decision records and research efforts live in the knowledge base (ADR 0290, issue 426), and the
+rules that judged them here are the knowledge base's now: `propose` refuses a record that breaks one.
+README.md beside this file says where each rule went. What stays here judges what this repository
+still holds, and keeps the records from coming back.
"""
import os
@@ -15,14 +20,17 @@ import sys
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
-# Where a citation is *guidance* rather than history. A document here tells somebody what to
-# do, so a link to a superseded record is an instruction to follow a withdrawn decision.
-# 02-DECISIONS and 01-RESEARCH are deliberately absent: they record what was decided and what
-# was observed, and both legitimately discuss superseded records at length.
-GOVERNING = ("00-META/", "03-DESIGN/01-to-be/")
-
LINK = re.compile(r"\[[^\]]*\]\((?!https?:|mailto:)([^)]+)\)")
-ADR_FILE = re.compile(r"^(\d{4})-")
+
+# The two folders the records left. Each keeps a README that points at the knowledge base, and
+# 02-DECISIONS/README.md also writes the reading order's topics, which the knowledge base reads.
+RECORD_FOLDERS = ("02-DECISIONS", "01-RESEARCH")
+POINTERS = {"02-DECISIONS/README.md", "01-RESEARCH/README.md"}
+
+# A decision record is named "ADR NNNN", four digits, and a research effort "research NNN", three.
+DECISION_ENTRY = re.compile(r"^ADR \d{4}$")
+ADR_MENTION = re.compile(r"\bADRs?\s+(\d+)\b")
+RESEARCH_MENTION = re.compile(r"\b[Rr]esearch\s+(\d+)\b")
def markdown_files():
@@ -45,7 +53,7 @@ def read(path):
def frontmatter(text):
"""Minimal frontmatter reader — enough for the fields these checks use.
- Not a YAML parser on purpose: a dependency in a repository that has none, to read four
+ Not a YAML parser on purpose: a dependency in a repository that has none, to read a few
scalar fields and one list, would cost more than it returns.
"""
if not text.startswith("---\n"):
@@ -74,24 +82,11 @@ def frontmatter(text):
return fields
-def load_records():
- """Every decision record, by its four-digit number."""
- records = {}
- folder = os.path.join(ROOT, "02-DECISIONS")
- for name in sorted(os.listdir(folder)):
- match = ADR_FILE.match(name)
- if not name.endswith(".md") or not match:
- continue
- path = os.path.join(folder, name)
- text = read(path)
- records[match.group(1)] = {
- "number": match.group(1),
- "name": name,
- "path": path,
- "text": text,
- "front": frontmatter(text),
- }
- return records
+def unquote(value):
+ value = value.strip()
+ if len(value) >= 2 and value[0] == value[-1] and value[0] in "\"'":
+ return value[1:-1]
+ return value
class Failures:
@@ -129,337 +124,62 @@ def check_links(failures):
failures.add("links", f"{rel(path)}:{number}", f"link does not resolve: {target}")
-def check_rests_on(failures, records):
- """A document's `decisions:` and a record's `extends:` must name a live record.
+def check_no_records(failures):
+ """No decision record or research effort is written into this repository again (ADR 0290 rule 9).
- These are the load-bearing citations: the document declares that it rests on that
- decision. Resting on a withdrawn one is the defect this whole check set exists for.
+ The knowledge base is the only place a record is read; a file here would be a second copy that
+ drifts from it, and nothing would read it. The folders keep their pointer READMEs and nothing else.
"""
- for path in markdown_files():
- front = frontmatter(read(path))
- cited = list(front.get("decisions", []) or [])
- extends = front.get("extends")
- if isinstance(extends, str) and extends:
- cited.append(extends)
-
- for entry in cited:
- match = ADR_FILE.match(os.path.basename(entry))
- if not match:
- failures.add("rests-on", rel(path), f"not a decision record: {entry}")
- continue
- number = match.group(1)
- if number not in records:
- failures.add("rests-on", rel(path), f"no such record: {entry}")
- continue
- status = records[number]["front"].get("status")
- if status != "accepted":
- # A proposed record may extend another proposed one. Decisions are drafted in
- # chains -- 0059 extends 0057 while both await review -- and refusing that would
- # mean either drafting out of order or marking records accepted to satisfy a
- # check, which is the failure this repository already made once.
- if frontmatter(read(path)).get("status") == "proposed":
- continue
- # An as-is document describes what runs, and what runs was built under
- # whatever was decided at the time. ADR 0056: "as-is describing a superseded
- # decision is exactly what as-is is for."
- if rel(path).startswith("03-DESIGN/00-as-is/"):
- continue
- # A withdrawn record's citations are history. It instructs nobody -- every reader
- # is sent to its superseder -- so what it was built on may itself be withdrawn.
- # Refusing that would mean rewriting the lineage of a record whose reasoning is
- # the thing the immutability rule protects.
- if frontmatter(read(path)).get("status") == "superseded":
- continue
- # An extension that supersedes legitimately names what it replaced.
- this = ADR_FILE.match(os.path.basename(path))
- supersedes = records[number]["front"].get("superseded-by", "")
- if this and supersedes and os.path.basename(path) in str(supersedes):
+ for folder in RECORD_FOLDERS:
+ base = os.path.join(ROOT, folder)
+ if not os.path.isdir(base):
+ failures.add("no-records", folder, "the folder is gone; it keeps a README that points at the "
+ "knowledge base (and, for 02-DECISIONS, the reading order the knowledge base reads)")
+ continue
+ for current, dirs, files in os.walk(base):
+ for name in sorted(files + [d + "/" for d in dirs]):
+ path = rel(os.path.join(current, name))
+ if path.rstrip("/") in POINTERS:
continue
failures.add(
- "rests-on",
- rel(path),
- f"rests on ADR {number}, which is '{status}' — a document may not rest on a "
- f"record that is not accepted",
+ "no-records",
+ path,
+ "a decision record or research effort lives in the knowledge base, never here: "
+ "write it with knowledge-base.reserve and knowledge-base.propose (playbooks 01 and 02)",
)
-def check_live_citations(failures, records):
- """In a governing document, a link to a superseded record must name its replacement.
+def check_decisions_entries(failures):
+ """A design's `decisions:` names each record as "ADR NNNN".
- The reader of a rule needs to know the rule was withdrawn, and needs somewhere to go.
- Naming the superseder in the same paragraph is both, and it is what a person would
- write anyway.
+ A to-be design rests on the records it names, and the knowledge base answers for a record by its
+ number; an entry in another form is a citation nothing can follow.
"""
- superseded = {
- number: record["front"].get("superseded-by", "")
- for number, record in records.items()
- if record["front"].get("status") == "superseded"
- }
-
for path in markdown_files():
- if not any(rel(path).startswith(prefix) for prefix in GOVERNING):
+ if not rel(path).startswith("03-DESIGN/"):
continue
+ for entry in frontmatter(read(path)).get("decisions", []) or []:
+ if not DECISION_ENTRY.match(unquote(entry)):
+ failures.add("decisions-form", rel(path),
+ f"decisions: entry {entry!r} is not 'ADR NNNN' (four digits)")
+
+
+def check_mentions(failures):
+ """In running text, a decision record is "ADR NNNN" (four digits) and a research effort "research NNN"
+ (three): the form the knowledge base and the issue tracker read a citation in. A code span is an example,
+ and is not read."""
+ for path in markdown_files():
text = read(path)
- offset = 0
- for paragraph in text.split("\n\n"):
- line_no = text[:offset].count("\n") + 1
- offset += len(paragraph) + 2
- targets = LINK.findall(paragraph)
- for target in targets:
- match = ADR_FILE.match(os.path.basename(target.split("#")[0]))
- if not match or match.group(1) not in superseded:
- continue
- number = match.group(1)
- replacement = os.path.basename(str(superseded[number]))
- if not replacement:
- failures.add(
- "live-citation",
- f"{rel(path)}:{line_no}",
- f"cites superseded ADR {number}, which names no superseder",
- )
- continue
- if not any(replacement in t for t in targets):
- failures.add(
- "live-citation",
- f"{rel(path)}:{line_no}",
- f"cites superseded ADR {number} without naming its replacement "
- f"({replacement}) in the same paragraph",
- )
-
-
-def check_supersession_symmetry(failures, records):
- """If A says it was superseded by B, B must say it supersedes A."""
- for number, record in records.items():
- front = record["front"]
- status = front.get("status")
- by = os.path.basename(str(front.get("superseded-by", "")))
-
- if status == "superseded" and not by:
- failures.add("supersession", rel(record["path"]), "marked superseded but names no superseder")
- continue
- if by and status != "superseded":
- failures.add("supersession", rel(record["path"]), f"names a superseder but status is '{status}'")
- if not by:
- continue
-
- match = ADR_FILE.match(by)
- if not match or match.group(1) not in records:
- failures.add("supersession", rel(record["path"]), f"superseder does not exist: {by}")
- continue
- other = records[match.group(1)]
- # `supersedes:` may name one record or several. One decision replacing two is a real
- # situation -- two records that built and refined the same wrong mechanism are withdrawn
- # by the one record that removes it -- and a check that allows only one would force
- # either a chain of pro-forma records or an unmarked supersession.
- claimed = other["front"].get("supersedes", "")
- if isinstance(claimed, str):
- claimed = [claimed] if claimed else []
- claims = [os.path.basename(str(entry)) for entry in claimed]
- if record["name"] not in claims:
- failures.add(
- "supersession",
- rel(other["path"]),
- f"ADR {number} says this supersedes it; this record does not say so "
- f"(supersedes: {', '.join(claims) or 'absent'})",
- )
-
-
-PARTIAL_MARK = re.compile(
- r"^> \*\*Superseded in part — (\d{4}-\d{2}-\d{2}), by \[ADR (\d{4})\]\(", re.M
-)
-
-
-# The form these notes had before the check: it named the later record but nothing read it.
-NARROWED_MARK = re.compile(r"^> \*\*Narrowed, not replaced — [^\n]*\[ADR \d{4}\]\(", re.M)
-
-
-def check_partial_supersession(failures, records):
- """A record superseded in part says so under its heading, and its superseder names it.
-
- A partial supersession was written only in the newer record's prose six times, and 11 of the
- 21 `supersedes-in-part:` links had no note on the record they change, so a reader who arrived
- at the older record by a citation read a rule that no longer held. Both directions are
- checked: the field on the newer record and the dated note on the older one, naming each other.
- The note is dated by the newer record's `date:`, and the older form of the note, which named a
- later record and was read by nothing, is refused so it cannot be a way round this check.
- """
- for number, record in records.items():
- claimed = record["front"].get("supersedes-in-part", [])
- if isinstance(claimed, str):
- claimed = [claimed] if claimed else []
- for entry in claimed:
- match = ADR_FILE.match(os.path.basename(entry))
- if not match or match.group(1) not in records:
- failures.add("partial-supersession", rel(record["path"]), f"no such record: {entry}")
- continue
- older = records[match.group(1)]
- marks = [by for _, by in PARTIAL_MARK.findall(older["text"])]
- if number not in marks:
- failures.add(
- "partial-supersession",
- rel(older["path"]),
- f"ADR {number} supersedes this in part; this record carries no "
- f"'> **Superseded in part — YYYY-MM-DD, by [ADR {number}](…).**' note",
- )
- for line in NARROWED_MARK.findall(record["text"]):
- failures.add(
- "partial-supersession",
- rel(record["path"]),
- f"a note names a later record in the old form ({line.strip()[:60]}…); write it as "
- f"'> **Superseded in part — YYYY-MM-DD, by [ADR NNNN](…).**' and declare it in that "
- f"record's `supersedes-in-part:`",
- )
- for date, by in PARTIAL_MARK.findall(record["text"]):
- if by in records and date != str(records[by]["front"].get("date", "")):
- failures.add(
- "partial-supersession",
- rel(record["path"]),
- f"the note for ADR {by} is dated {date}; ADR {by} is dated "
- f"{records[by]['front'].get('date', 'nowhere')}",
- )
- if by not in records:
- failures.add("partial-supersession", rel(record["path"]), f"note names no such record: ADR {by}")
- continue
- names = records[by]["front"].get("supersedes-in-part", [])
- if isinstance(names, str):
- names = [names] if names else []
- if record["name"] not in [os.path.basename(str(n)) for n in names]:
- failures.add(
- "partial-supersession",
- rel(records[by]["path"]),
- f"ADR {number} says this supersedes it in part; this record's "
- f"`supersedes-in-part:` does not name it",
- )
-
-
-CLAUSES_FROM = "2026-10-11"
-DECISION_SECTION = re.compile(r"^## Decision[^\n]*\n(.*?)(?=^## |\Z)", re.M | re.S)
-CLAUSE = re.compile(r"^\d+\. \*\*", re.M)
-
-
-def check_clauses(failures, records):
- """A record dated from 2026-10-11 writes its decision as numbered clauses (ADR 0290 rule 3).
-
- The knowledge base reads a decision's clauses from the merged text, so a clause's wording is the reviewed
- one. 226 of 263 records written before then are prose, and their clauses had to be drafted and confirmed
- instead; a new record is not added to them.
- """
- for number, record in records.items():
- front = record["front"]
- if front.get("status") != "accepted" or str(front.get("date", "")) < CLAUSES_FROM:
- continue
- section = DECISION_SECTION.search(record["text"])
- if not section or not CLAUSE.search(section.group(1)):
- failures.add(
- "clauses",
- rel(record["path"]),
- "dated from 2026-10-11, and its `## Decision` section has no numbered clause "
- "('1. **…**'); write the decision as numbered clauses with a bold lead",
- )
-
-
-def check_topics(failures, records):
- """Every record names a topic of the reading order.
-
- The topic is what puts a record in the reading order, so a record without one — or with one
- nobody defined — disappears from the generated list rather than appearing in the wrong place.
- That is the quiet failure, so it is the one checked. The topics are the ones
- `02-DECISIONS/README.md` writes down, read the way `index.py` reads them, so there is one list.
- """
- from index import reading_order
- known = {topic for topic, _ in reading_order()}
- for number, record in sorted(records.items()):
- topic = record["front"].get("topic")
- if not topic:
- failures.add("topics", rel(record["path"]),
- "no topic, so it has no place in the reading order")
- elif topic not in known:
- failures.add("topics", rel(record["path"]),
- "topic %r is not one of: %s" % (topic, ", ".join(sorted(known))))
-
-
-def check_numbering(failures, records):
- """The number in the filename is the number in the heading."""
- for number, record in records.items():
- heading = re.search(r"^# (\d+)\.", record["text"], re.M)
- if not heading:
- failures.add("numbering", rel(record["path"]), "no '# N. Title' heading")
- elif heading.group(1) != number.lstrip("0"):
- failures.add(
- "numbering",
- rel(record["path"]),
- f"filename says {number}, heading says {heading.group(1)}",
- )
-
-
-def check_progressive_insights(failures, records):
- """A correction made inside a record is marked and dated, or it is a silent rewrite.
-
- A record may be corrected in place when a *fact* in it went stale and the decision still
- stands (`02-DECISIONS/README.md`, "Progressive insight"). The whole safety of that allowance
- is that the correction is legible in the record rather than only in a diff nobody reads, so
- the form is what is checked here: every mention of an insight is the marker, the marker
- carries an ISO date, and that date is not earlier than the decision's own — an insight
- predating the decision it corrects is a copied marker, not a correction.
-
- What this cannot check is an edit made with no marker at all. Nothing mechanical can; that
- one is the reviewer's, reading the diff. The check keeps the *marked* path honest so that an
- unmarked change stands out as the anomaly it is.
- """
- phrase = re.compile(r"progressive insight", re.I)
- # Both patterns stay on one line: a bold run does not span paragraphs, and `[^*]*` across
- # newlines will happily join an unrelated `**` far above to the marker below, reporting the
- # whole span between them. It did exactly that the first time this ran.
- # Trailing words after the date are allowed — "— 2026-09-26, correcting the one above." — so
- # an insight can say what it relates to. Only the date's presence and position are fixed.
- marker = re.compile(r"\*\*Progressive insights?[ \t]*[\u2014\u2013-][ \t]*(\d{4}-\d{2}-\d{2})[^*\n]*\*\*")
- loose = re.compile(r"\*\*[^*\n]*[Pp]rogressive insights?[^*\n]*\*\*")
- iso = re.compile(r"^\d{4}-\d{2}-\d{2}$")
-
- for number, record in sorted(records.items()):
- text = record["text"]
- if not phrase.search(text):
- continue
- decided = str(record["front"].get("date", ""))
- good = [(m.start(), m.end(), m.group(1)) for m in marker.finditer(text)]
-
- for m in loose.finditer(text):
- if any(s <= m.start() and m.end() <= e for s, e, _ in good):
- continue
- # A bold run carrying a link is discussing an insight — usually another record's —
- # rather than marking one. A marker never needs to cite anything.
- if "](" in m.group(0):
- continue
- failures.add("insights", rel(record["path"]),
- "a progressive insight is not in the dated marked form "
- "'**Progressive insight \u2014 YYYY-MM-DD.**': %s" % m.group(0))
-
- for _, _, stamp in good:
- if decided and iso.match(decided) and stamp < decided:
- failures.add("insights", rel(record["path"]),
- "a progressive insight dated %s predates the decision (%s)"
- % (stamp, decided))
-
- covered = [(s, e) for s, e, _ in good]
- for m in phrase.finditer(text):
- if any(s <= m.start() and m.end() <= e for s, e in covered):
- continue
- line = text.rfind("\n", 0, m.start()) + 1
- end = text.find("\n", m.end())
- whole = text[line:end if end != -1 else len(text)]
- if whole.lstrip().startswith("#"):
- continue
- # A line that also carries a link is discussing the rule, not marking a correction:
- # a marker never needs to cite anything, and a record that reasons about the policy
- # must be able to name it. Bare prose with no citation is the informal marking this
- # is here to catch.
- if "](" in whole:
- continue
- if loose.search(text, line, text.find("\n", m.end()) + 1 or len(text)):
- continue
- failures.add("insights", rel(record["path"]),
- "'progressive insight' appears unmarked; a correction is marked and "
- "dated, or it is a silent rewrite")
+ for number, line in enumerate(text.split("\n"), 1):
+ line = re.sub(r"`[^`]*`", "", line)
+ for m in ADR_MENTION.finditer(line):
+ if len(m.group(1)) != 4:
+ failures.add("mention-form", f"{rel(path)}:{number}",
+ f"{m.group(0)!r}: a decision record is named 'ADR NNNN', four digits")
+ for m in RESEARCH_MENTION.finditer(line):
+ if len(m.group(1)) != 3:
+ failures.add("mention-form", f"{rel(path)}:{number}",
+ f"{m.group(0)!r}: a research effort is named 'research NNN', three digits")
def check_status_against_code(failures):
@@ -495,18 +215,11 @@ def check_status_against_code(failures):
def main():
failures = Failures()
- records = load_records()
check_links(failures)
- check_rests_on(failures, records)
- check_live_citations(failures, records)
- check_supersession_symmetry(failures, records)
- check_partial_supersession(failures, records)
- check_clauses(failures, records)
- check_numbering(failures, records)
- check_topics(failures, records)
+ check_no_records(failures)
+ check_decisions_entries(failures)
+ check_mentions(failures)
check_status_against_code(failures)
- check_progressive_insights(failures, records)
- print(f"records: {len(records)} decision records checked")
return failures.report()
diff --git a/00-META/checks/words-allowed.md b/00-META/checks/words-allowed.md
index b3d99a77..5768351d 100644
--- a/00-META/checks/words-allowed.md
+++ b/00-META/checks/words-allowed.md
@@ -1,17 +1,10 @@
# Words allowed — documents `words.py` does not hold to the glossary's retired words
-Read by [`words.py`](words.py) ([ADR 0244](../../02-DECISIONS/0244-the-mesh-is-described-in-domains-and-one-word-names-one-thing.md)).
+Read by [`words.py`](words.py) (ADR 0244).
One row per document. *Until* is the date by which it is reworded — past it, the row fails like the words
-themselves — or `kept` for a graduated research effort, which records what was said and keeps its words
-the way a decision record does. A row whose document no longer uses a retired word fails too, so this
-list only shrinks.
+themselves. A row whose document no longer uses a retired word fails too, so this list only shrinks. The
+research efforts this list kept as written (research 034 and 042) live in the knowledge base now (ADR 0290),
+which keeps a record's words as it was imported.
| Document | Until | Why |
|---|---|---|
-| `01-RESEARCH/034-the-mesh-in-domains/00-overview.md` | kept | the effort that studied these words; its findings quote and propose them as they stood before ADR 0244 |
-| `01-RESEARCH/034-the-mesh-in-domains/01-the-concepts-in-use.md` | kept | as above: the inventory of the words in use |
-| `01-RESEARCH/034-the-mesh-in-domains/02-the-domains.md` | kept | as above: the proposed domains, in the words of the day |
-| `01-RESEARCH/034-the-mesh-in-domains/03-the-clashes.md` | kept | as above: every clash names the words that clashed |
-| `01-RESEARCH/034-the-mesh-in-domains/04-how-the-glossary-is-checked.md` | kept | as above: the check's own examples |
-| `01-RESEARCH/034-the-mesh-in-domains/05-a-glossary-by-domain.md` | kept | as above: the proposed glossary, with the entries of the day |
-| `01-RESEARCH/042-the-mesh-keeps-its-own-issues/00-overview.md` | kept | the effort that settled the word: it records the operator's ask in the words it was made in |
diff --git a/00-META/checks/words.py b/00-META/checks/words.py
index bc4450ef..4e209ef6 100644
--- a/00-META/checks/words.py
+++ b/00-META/checks/words.py
@@ -42,14 +42,14 @@ What is enforced:
subcommands (`nox-mesh-host enrol`, `key`, `reconcile`, …) are not lines here: they are
typed at a machine no mesh reaches yet, and the engine's usage says so.
-Checked documents: 00-META/, 03-DESIGN/ (both layers), AGENTS.md and README.md, always; a research
-effort initiated, or an issue opened, on or after FROM (on or after LINES_FROM for the lines). Never
-02-DECISIONS/: a decision record keeps the words it was written with.
+Checked documents: 00-META/, 03-DESIGN/ (both layers), AGENTS.md and README.md, always; an issue opened
+on or after FROM (on or after LINES_FROM for the lines). The decision records and research efforts live in
+the knowledge base (ADR 0290, issue 426), which holds a record proposed there to the same words through its
+rules `retired-word` and `daemon-line`, read from this glossary.
A document the check fails on that cannot be reworded in the change that found it is named, with a date
and a reason, in words-allowed.md beside this file. An entry past its date fails like the word itself, and
-an entry for a document that no longer needs it fails too. `kept` instead of a date is allowed only for a
-graduated research effort, which is a record of what was said, like a decision record.
+an entry for a document that no longer needs it fails too.
python3 00-META/checks/words.py
python3 00-META/checks/words.py --list tools
@@ -65,7 +65,7 @@ ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", ".."))
GLOSSARY = "00-META/glossary.md"
ALLOWED = "00-META/checks/words-allowed.md"
-# The day the rule began (ADR 0244): research and issues from it on are held to it.
+# The day the rule began (ADR 0244): issues opened from it on are held to it.
FROM = "2026-10-07"
ALWAYS = ("00-META/", "03-DESIGN/", "AGENTS.md", "README.md")
@@ -168,21 +168,22 @@ def prose(text, glossary_file=False):
return text
-def research_and_issues(since=FROM):
- """The research efforts and issue reports dated on or after `since`, file by file."""
+def issues(since=FROM):
+ """The issue reports opened on or after `since`, file by file."""
out = []
- for folder, key, first in (("01-RESEARCH", "initiated", "00-overview.md"), ("04-ISSUES", "opened", "00-report.md")):
- base = os.path.join(ROOT, folder)
- for effort in sorted(os.listdir(base)):
- head = os.path.join(base, effort, first)
- if not os.path.isfile(head):
- continue
- date = frontmatter_field(read(rel(head)), key)
- if not date or date < since:
- continue
- for name in sorted(os.listdir(os.path.join(base, effort))):
- if name.endswith(".md"):
- out.append("%s/%s/%s" % (folder, effort, name))
+ base = os.path.join(ROOT, "04-ISSUES")
+ if not os.path.isdir(base):
+ return out
+ for issue in sorted(os.listdir(base)):
+ head = os.path.join(base, issue, "00-report.md")
+ if not os.path.isfile(head):
+ continue
+ date = frontmatter_field(read(rel(head)), "opened")
+ if not date or date < since:
+ continue
+ for name in sorted(os.listdir(os.path.join(base, issue))):
+ if name.endswith(".md"):
+ out.append("04-ISSUES/%s/%s" % (issue, name))
return out
@@ -198,15 +199,13 @@ def checked_documents(since=FROM):
for name in sorted(files):
if name.endswith(".md"):
docs.append(rel(os.path.join(base, name)))
- return docs + research_and_issues(since)
+ return docs + issues(since)
def allowed():
"""words-allowed.md: one row per document — | `document` | until | why |.
- `until` is a date, after which the allowance fails like the words themselves, or `kept` for a
- document that records what was said and must keep its words: only a research effort that has
- graduated may be kept, because once it has become a decision it is a record like one.
+ `until` is a date, after which the allowance fails like the words themselves.
"""
out, problems = {}, []
if not os.path.isfile(os.path.join(ROOT, ALLOWED)):
@@ -219,14 +218,8 @@ def allowed():
doc, until, why = cells[0].strip("`"), cells[1], cells[2]
if not why:
problems.append("%s: the allowance for %s gives no reason" % (ALLOWED, doc))
- if until == "kept":
- overview = os.path.join(os.path.dirname(doc), "00-overview.md")
- status = frontmatter_field(read(overview), "status") if os.path.isfile(os.path.join(ROOT, overview)) else None
- if not doc.startswith("01-RESEARCH/") or status != "graduated":
- problems.append("%s: %s is kept, but only a graduated research effort may be" % (ALLOWED, doc))
- continue
- elif not re.match(r"^\d{4}-\d{2}-\d{2}$", until):
- problems.append("%s: the allowance for %s has neither a date nor `kept`" % (ALLOWED, doc))
+ if not re.match(r"^\d{4}-\d{2}-\d{2}$", until):
+ problems.append("%s: the allowance for %s has no date" % (ALLOWED, doc))
continue
elif until < today:
problems.append("%s: the allowance for %s ran out on %s — reword it" % (ALLOWED, doc, until))
diff --git a/00-META/glossary.md b/00-META/glossary.md
index d2ed9f0d..cfbd12f1 100644
--- a/00-META/glossary.md
+++ b/00-META/glossary.md
@@ -4,7 +4,7 @@ One name per thing. This page is the authority; where an older record says somet
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);
+its check are ADR 0244;
the domains are drawn in [to-be 49](../03-DESIGN/01-to-be/49-the-mesh-in-domains.md).
## How to read this page
@@ -30,33 +30,33 @@ is listed under [Homonyms](#homonyms), with the qualified form each 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)).
+ in the section above (ADR 0244).
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)).
+ the controller, the domain owns that store alone (ADR 0008).
*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
+ (ADR 0004), 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).
+ it what it is) — ADR 0100.
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.
+ (ADR 0040). 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
+ (ADR 0126). 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)).
+ promised as optional, then served, then required (ADR 0246).
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)).
+ not declare them is refused, and only a claimant declares them (ADR 0287).
- **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
@@ -81,15 +81,15 @@ are a published vocabulary, fixed by the manifest's schema. **Decided by** ADR 0
- **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)).
+ `mesh-store` and `mesh-broker` seats (ADR 0079,
+ ADR 0116).
- **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)).
+ a module (ADR 0174).
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)).
+ (ADR 0262).
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
@@ -98,7 +98,7 @@ are a published vocabulary, fixed by the manifest's schema. **Decided by** ADR 0
- **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)).
+ (ADR 0188).
- **tool** — one capability a module serves through the tool runner, addressed `.`. 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
@@ -108,7 +108,7 @@ are a published vocabulary, fixed by the manifest's schema. **Decided by** ADR 0
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)).
+ (ADR 0236).
The controller's verb `upgrade` shows it.
## Core — the mesh's own machinery
@@ -142,7 +142,7 @@ every other domain has something to run on. **Recorded by** the controller and t
- **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),
+ (ADR 0175,
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
@@ -151,21 +151,21 @@ every other domain has something to run on. **Recorded by** the controller and t
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)).
+ done by hand (ADR 0067).
- **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));
+ control, declarations, builds, events, tool calls (ADR 0106).
+ A module reaches it by requiring `mesh-bus` (ADR 0128);
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
+ (ADR 0131) — 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
@@ -175,13 +175,13 @@ every other domain has something to run on. **Recorded by** the controller and t
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)).
+ (ADR 0229).
- **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
+ (ADR 0289). 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.
@@ -195,23 +195,23 @@ and Health and repair. **Decided by** ADR 0100, 0126, 0176, 0181, 0207, 0221, 02
- **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)).
+ (ADR 0261).
- **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
+ (ADR 0223). 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)).
+ — on that node (ADR 0176).
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)).
+ (ADR 0207).
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
@@ -224,16 +224,16 @@ and Health and repair. **Decided by** ADR 0100, 0126, 0176, 0181, 0207, 0221, 02
- **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)).
+ (ADR 0181).
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)).
+ every look (ADR 0266).
- **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)).
+ (ADR 0252).
Never *membership*, which is the bus's word.
## Provisioning — one module serving another
@@ -249,15 +249,15 @@ and Health and repair. **Decided by** ADR 0100, 0126, 0176, 0181, 0207, 0221, 02
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)).
+ (ADR 0084).
- **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)).
+ reaches (ADR 0138).
- **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)).
+ only by a person (ADR 0230).
- **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)).
+ (ADR 0263).
- **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;
@@ -274,31 +274,31 @@ and the operator act. **Recorded by** the controller's `identity` and `licences`
- **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)).
+ file at apply (ADR 0085).
- **secret family** — a module's own secrets declared as one name ending in `-*`; each member is the prefix
and a name (`smb-password-`, 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)).
+ (ADR 0283).
- **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)).
+ (ADR 0225).
- **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)).
+ coding agent on a node (ADR 0183).
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),
+ ADR 0277,
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)).
+ (ADR 0272).
## Change and delivery — a commit on its way to the nodes
@@ -311,10 +311,10 @@ planner, the walk). **Downstream of** Module, Core, Health and repair and Data.
- **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)).
+ (ADR 0238).
- **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)).
+ (ADR 0237).
*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
@@ -324,14 +324,14 @@ planner, the walk). **Downstream of** Module, Core, Health and repair and Data.
- **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)).
+ (ADR 0156).
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)).
+ protocols, not one store being weak (ADR 0075).
- **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)).
+ (ADR 0267).
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
@@ -341,20 +341,20 @@ planner, the walk). **Downstream of** Module, Core, Health and repair and Data.
*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)).
+ (ADR 0239).
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)).
+ (ADR 0276).
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
+ ADR 0250,
+ ADR 0270). 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)).
+ ends in (ADR 0270).
- **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.
@@ -365,7 +365,7 @@ planner, the walk). **Downstream of** Module, Core, Health and repair and Data.
*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)).
+ 10 minutes after its first merge (ADR 0276).
- **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.
@@ -373,7 +373,7 @@ planner, the walk). **Downstream of** Module, Core, Health and repair and Data.
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
+ ADR 0276). 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.
@@ -383,7 +383,7 @@ planner, the walk). **Downstream of** Module, Core, Health and repair and Data.
- **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).
+ ADR 0282.
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
@@ -405,7 +405,7 @@ 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)).
+ node-engine judges (ADR 0240).
- **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).
@@ -413,14 +413,14 @@ becomes a message). **Decided by** ADR 0227, 0231, 0240, 0252, 0283.
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)).
+ (ADR 0283).
- **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)).
+ (ADR 0231).
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
@@ -432,7 +432,7 @@ becomes a message). **Decided by** ADR 0227, 0231, 0240, 0252, 0283.
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)).
+ operator's decision (ADR 0259).
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
@@ -447,13 +447,13 @@ becomes a message). **Decided by** ADR 0227, 0231, 0240, 0252, 0283.
- **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)).
+ (ADR 0233).
- **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)).
+ (ADR 0235).
- **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)).
+ person (ADR 0232).
## Connectivity — how nodes and modules reach one another
@@ -463,19 +463,19 @@ it should not. **Recorded by** the controller. **Upstream of** Provisioning and
**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)).
+ address and a mesh name (ADR 0226).
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,
+ connection to the outside network, a seat (ADR 0117). **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)).
+ something requires it (ADR 0247).
- **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
@@ -504,26 +504,26 @@ asking, never the act. **Decided by** ADR 0152, 0175, 0234, 0259, 0272.
`/usr/local/bin/mesh-cli ` 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)).
+ (ADR 0272).
- **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),
+ (ADR 0272,
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)).
+ sender (ADR 0234).
- **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)).
+ (ADR 0253).
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
@@ -534,12 +534,12 @@ asking, never the act. **Decided by** ADR 0152, 0175, 0234, 0259, 0272.
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)).
+ 0274), a trusted setting's layer (ADR 0277).
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)).
+ (ADR 0286).
- **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
@@ -552,12 +552,12 @@ asking, never the act. **Decided by** ADR 0152, 0175, 0234, 0259, 0272.
- **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.
+ proves itself (ADR 0274). 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)).
+ (ADR 0273).
- **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
@@ -569,15 +569,16 @@ Not a domain of the mesh but of the way it is built, listed because its words me
**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/`;
+- **research effort**, **graduation**, **hand-off**, **playbook** — an investigation, held whole in the
+ knowledge base and named "research NNN";
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
+- **decision record** — one numbered record in the knowledge base, named "ADR NNNN", 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
+ (ADR 0290). 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,
@@ -586,7 +587,7 @@ Not a domain of the mesh but of the way it is built, listed because its words me
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));
+ holds it whole: its report, diagnosis and resolution included (ADR 0291);
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`,
@@ -595,20 +596,20 @@ Not a domain of the mesh but of the way it is built, listed because its words me
*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)).
+ issue (ADR 0278).
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)).
+ depends on or relates to; it fills only what nobody gave (ADR 0279).
- **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)).
+ is worked on; an issue is resolved only from it (ADR 0284).
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)).
+ where each stands, or that none does (ADR 0285).
- **hq check** — one of `00-META/checks/`, run by `merge-check.sh` as this repository's check.
## Homonyms
@@ -667,14 +668,15 @@ renaming one sense, the old sense moves to a *Not:* line and becomes mechanical.
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.
+decision record keeps whatever word it was written with — those are immutable — so a word
+retired here may still appear in one, 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)):
+**How it is checked** (ADR 0244):
`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
+issues opened 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.
+A decision record or research effort proposed to the knowledge base is held to the same words by its
+rules `retired-word` and `daemon-line`, which read this page (ADR 0290). Homonyms are checked by review.
diff --git a/00-META/how-we-build.md b/00-META/how-we-build.md
index 53bd6d82..6c105466 100644
--- a/00-META/how-we-build.md
+++ b/00-META/how-we-build.md
@@ -3,7 +3,7 @@ status: canonical
updated: 2026-08-23
derives: knowledge-base constitution page
decisions:
- - 02-DECISIONS/0020-the-mesh-is-governed-by-a-constitution.md
+ - "ADR 0020"
---
# How we build
@@ -37,13 +37,13 @@ incident behind it is not written down, and the fix is to write it down, not to
| Rule | What it means |
|---|---|
| **Never write to a production database directly** | No insert, update, delete or schema statement executed against production by hand. Schema changes go through numbered migrations; data changes go through application code or the module's own capabilities. Raw statements skip every side effect the proper path has — events, audit, cache invalidation, fan-out. |
-| **Every schema change is a migration** | Numbered, in the module's own language, compiled with it. Both a baseline for a fresh installation *and* an incremental migration for installations that already exist. If code references a column, the migration creating it must exist. [ADR 0013](../02-DECISIONS/0013-schema-changes-are-numbered-migrations.md) |
+| **Every schema change is a migration** | Numbered, in the module's own language, compiled with it. Both a baseline for a fresh installation *and* an incremental migration for installations that already exist. If code references a column, the migration creating it must exist. ADR 0013 |
| **Never bypass the pipeline** | No manual database edit, no manual restart as a workaround. Fix the cause and deploy. A workaround that works is a workaround that is never removed, and the next person cannot tell the node from its declaration. |
-| **Never create a symlink** | A hand-made link caused production data loss through container volume resolution, and the judgement needed to make a safe exception is exactly the judgement unavailable at the moment it matters. **The mesh creates none at all** ([ADR 0012](../02-DECISIONS/0012-the-mesh-creates-no-symlinks.md), which supersedes [ADR 0012](../02-DECISIONS/0012-the-mesh-creates-no-symlinks.md)). The links the installer still reconciles are a migration, not a permission. |
+| **Never create a symlink** | A hand-made link caused production data loss through container volume resolution, and the judgement needed to make a safe exception is exactly the judgement unavailable at the moment it matters. **The mesh creates none at all** (ADR 0012, which supersedes ADR 0012). The links the installer still reconciles are a migration, not a permission. |
| **Never push directly to the main branch** | Branch, push, review, merge. Every merge is a human checkpoint, without exception — **including in this repository**. A documentation repository is not a lower tier of care; a decision record lands the same way a service does. |
-| **One change per pull request, and never merge unapproved work** | Unrelated improvements bundled together cannot be reviewed or reverted separately. And the checkpoint is **a person deciding, not a person clicking** — work may be merged by whoever wrote it once a human has explicitly approved *that merge*, and never on a standing permission, an instruction to do the work, silence, or the author's own judgement that it is ready. [ADR 0023](../02-DECISIONS/0023-approval-is-the-checkpoint.md) |
+| **One change per pull request, and never merge unapproved work** | Unrelated improvements bundled together cannot be reviewed or reverted separately. And the checkpoint is **a person deciding, not a person clicking** — work may be merged by whoever wrote it once a human has explicitly approved *that merge*, and never on a standing permission, an instruction to do the work, silence, or the author's own judgement that it is ready. ADR 0023 |
| **Never open a pull request unprompted** | A permissions list saying it is allowed is not a request. |
-| **A failed step fails the job** | A sequence that continues past a failure does the next thing in the wrong place. Gate each step on the last. [ADR 0010](../02-DECISIONS/0010-delivery.md), and §5. |
+| **A failed step fails the job** | A sequence that continues past a failure does the next thing in the wrong place. Gate each step on the last. ADR 0010, and §5. |
### A failed step must stop the steps after it — how it was earned
@@ -69,7 +69,7 @@ reported failure, nothing stopped, and the damage happened somewhere nobody was
- **Every runtime variable is declared.** A variable the module reads and the manifest does not
declare is invisible to the mesh: it will not be generated, injected, or audited.
- **Provisioned credentials arrive through declared requirements**, never hardcoded in code,
- compose files or scripts. [ADR 0009](../02-DECISIONS/0009-modules-and-the-graph.md)
+ compose files or scripts. ADR 0009
- **Never install a package by hand.** A package is declared in the manifest and arrives the
way every other package does. A hand-installed package is invisible to the mesh: it is not
declared, not reproduced on the next node, and not present after a rebuild — and the node
@@ -82,7 +82,7 @@ reported failure, nothing stopped, and the damage happened somewhere nobody was
- **Every standalone application gets its own repository**, with a manifest at its root,
registered as a build source. Creating an application directory in the monorepo is a
convention violation and reviewers reject it.
- [ADR 0015](../02-DECISIONS/0015-applications-live-in-their-own-repository.md)
+ ADR 0015
### Migrations
@@ -96,7 +96,7 @@ reported failure, nothing stopped, and the damage happened somewhere nobody was
surface is regenerated from the mesh database; a local edit survives one synchronisation and is
then silently overwritten, bringing back whatever it fixed. Use the mesh operation that owns
the value. If unsure whether a file is managed, ask the tooling — the answer is not visible
-from the file. [ADR 0011](../02-DECISIONS/0011-managed-files-are-generated-never-edited.md)
+from the file. ADR 0011
---
@@ -121,7 +121,7 @@ for them. Do not merge them into one module: they are delivered to different nod
that must be assigned where half of it is unwanted is not a boundary either.
Coherence is a context. Delivery is a module. Relationships are edges, not folders.
-[ADR 0009](../02-DECISIONS/0009-modules-and-the-graph.md)
+ADR 0009
### Contexts integrate through the record, never through a shared schema
@@ -214,7 +214,7 @@ This document is governed. It does not change by commit message or unilateral de
1. **Propose** — a change stating what rule is changing, why the current wording is
inadequate, and what reviewed it.
2. **Review** — sign-off by reviewers who are not the proposer.
-3. **Record** — the change is a decision and gets a record in [`02-DECISIONS/`](../02-DECISIONS/), because a
+3. **Record** — the change is a decision and gets a decision record in the knowledge base (playbook [02](process/02-graduation.md)), because a
rule the mesh enforces is architecturally significant.
4. **Sync** — playbook [`process/05-constitution-sync.md`](process/05-constitution-sync.md)
publishes the derived page. An unsynced rule is a rule the mesh does not enforce, whatever
@@ -226,7 +226,7 @@ No drive-by edits. Every change traces to a recorded decision.
a design meeting with at least two node operators — which has never been met and cannot be, as
there is one operator. A rule that cannot be satisfied is not a high standard; it is a rule
everything silently violates. Recorded here as resolved in favour of what is achievable, and
-what has in fact been practised ([ADR 0022](../02-DECISIONS/0022-the-constitution-absorbs-what-is-enforced.md)).
+what has in fact been practised (ADR 0022).
---
@@ -243,7 +243,7 @@ process. Absence of an override means these rules apply unmodified.
## 8. Code quality
*Absorbed 2026-08-26 from the enforced page, which carried these rules while this document did
-not — [ADR 0022](../02-DECISIONS/0022-the-constitution-absorbs-what-is-enforced.md).*
+not — ADR 0022.*
**These rules are recorded because they are enforced, not because this repository earned them.**
Every other rule here states the incident or measurement behind it. These state nothing,
@@ -272,7 +272,7 @@ Data access, business logic and the interface layer are separate.
*Scope: the mesh's services and surfaces. Tier 0 is a statically linked binary that must depend
on nothing installed first, and is written in Go —
-[ADR 0005](../02-DECISIONS/0005-the-node-host.md).*
+ADR 0005.*
- TypeScript throughout; no new untyped JavaScript.
- Strict, with no implicit `any` and no unchecked index access.
diff --git a/00-META/process/00-overview.md b/00-META/process/00-overview.md
index 0fc8a892..c0a4f145 100644
--- a/00-META/process/00-overview.md
+++ b/00-META/process/00-overview.md
@@ -3,8 +3,8 @@
How work moves through HQ, and who may do what. In industry terms this is **spec-driven
development, with provenance**: the decision is the why, the design doc is the spec, `code:`
names the implementation, and the lab beds are the conformance tests — and unlike the common
-form, the chain itself is checked ([ADR 0080](../../02-DECISIONS/0080-the-development-cycle-is-checked.md),
-[0081](../../02-DECISIONS/0081-a-decision-nothing-cites-is-not-yet-in-the-chain.md)). Every other document in this folder is a
+form, the chain itself is checked (ADR 0080,
+ADR 0081). Every other document in this folder is a
playbook: trigger, who runs it, steps, outputs. Engineers and agents follow the same
playbooks; agents must not act outside them.
@@ -19,11 +19,13 @@ playbooks; agents must not act outside them.
## The knowledge flow
```
-idea ──► 01-RESEARCH ──► decision (02-DECISIONS/) ──► 03-DESIGN/01-to-be ──► built (code repo)
- │ │ │
- │ │ └─► 03-DESIGN/00-as-is once shipped
+idea ──► research ──► decision ──► 03-DESIGN/01-to-be ──► built (code repo)
+ │ │ │
+ │ │ └─► 03-DESIGN/00-as-is once shipped
│ └────► abandoned (recorded, kept)
- └─(small/obvious, still recorded in 02-DECISIONS)──────────► 03-DESIGN directly
+ └─(small/obvious, still a decision record)──────────► 03-DESIGN directly
+
+research and decisions: in the knowledge base (knowledge-base.reserve, propose, ask, get, holds)
symptom ──► 04-ISSUES ──► diagnosis ──► code-repo fix and/or design amendment
@@ -37,7 +39,7 @@ how-we-build.md ──► constitution sync ──► knowledge base ──► i
| Layer | What it is | Changes when |
|---|---|---|
| `00-as-is/` | The mesh that exists today. Describes shipped behaviour, including behaviour nobody would choose again. | Something ships, or an as-is claim is found to be wrong. |
-| `01-to-be/` | The mesh being built toward. Every statement traceable to a record in `02-DECISIONS/`. | A decision is taken or amended. |
+| `01-to-be/` | The mesh being built toward. Every statement traceable to a decision record in the knowledge base. | A decision is taken or amended. |
A to-be document that ships does not move. Its as-is counterpart is written or updated, the
to-be document's frontmatter goes to `implemented`, and both stand — one describing what runs,
@@ -61,23 +63,24 @@ the expensive half.
The flow above is a rule, and a rule states how it is checked:
[`00-META/checks/cycle.py`](../checks/cycle.py) refuses a to-be design that names no
decision, an `in-progress`/`implemented` design that names no owning code, two issue folders
-sharing a number or an issue report carrying a status key, and a `graduated`
-research overview that does not say what it became. Run it with `records.py`, `index.py` and
+sharing a number or an issue report carrying a status key; a `graduated` research effort that
+does not say what it became is refused by the knowledge base's `propose` (ADR 0290). Run it with `records.py`, `index.py` and
`words.py` before any HQ merge (`sh merge-check.sh` runs all four). The issue rules the check
once held — `located` with an owner, `resolved` with a fix, a core issue with its replay — are
refused at the write by the issue tracker's `move`
-([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)). What the checks cannot see — that code work actually started from a
+(ADR 0275). What the checks cannot see — that code work actually started from a
handoff — is held by playbooks [04](04-build-handoff.md) and [07](07-feature-branches.md):
a feature branch exists because a design or an issue sent it.
## Status lives in frontmatter, an issue's in the tracker
-Research overviews, design docs and decision records each carry their status as
-YAML frontmatter (schemas in the section READMEs and playbooks). An issue's status lives in the
+Design docs carry their status as YAML frontmatter (schema in playbook [02](02-graduation.md)).
+Research efforts and decision records carry theirs in the knowledge base, which refuses a wrong one at
+`propose` (ADR 0290). An issue's status lives in the
issue tracker, which refuses a wrong status at the write, and its report here carries only
the day it was opened (playbook [03](03-issues.md)). There are **no central status
files** and no decision ledger. **Every decision is a record in
-[`02-DECISIONS`](../../02-DECISIONS/)** — if it is worth recording it is worth a record, and if
-it is not worth a record it is not recorded. Cross-cutting views, the decision index included,
-are generated on demand by the `hq-status` skill and never written to disk
-([ADR 0248](../../02-DECISIONS/0248-the-decision-index-is-generated-when-it-is-read-and-never-stored.md)).
+the knowledge base** — if it is worth recording it is worth a record, and if
+it is not worth a record it is not recorded. Cross-cutting views are generated on demand by the
+`hq-status` skill and never written to disk (ADR 0248); the decisions of a topic are
+`knowledge-base.holds` with that topic.
diff --git a/00-META/process/01-research.md b/00-META/process/01-research.md
index 0241f43c..98ab7ebc 100644
--- a/00-META/process/01-research.md
+++ b/00-META/process/01-research.md
@@ -5,25 +5,34 @@ design. Also: an as-is document that raises a question nobody can answer.
**Who runs it.** Anyone.
+**Where it lives.** A research effort lives whole in the knowledge base, the seat `knowledge-base`, reached
+through the mesh MCP server from any node and any folder (ADR 0290). This repository holds none: an effort
+is written with the knowledge base's verbs, never as a folder here.
+
## Steps
-1. Take the next free number: highest `01-RESEARCH/NNN-*` plus one, zero-padded to three
- digits. Never skip or reuse a number.
-2. Create `01-RESEARCH/NNN-descriptive-name/` and a `00-overview.md` in it carrying:
+1. **Read what is known first.** `knowledge-base.ask` with the question in plain words returns the decision
+ records and research efforts that bear on it; `knowledge-base.get` with `kind: research` and a number
+ reads one effort, and `reasoning: true` (or `section` with a document's name) reads its documents.
+2. **Take a number.** `knowledge-base.reserve` with `kind: research` and `for`: the effort's title. The
+ number comes from the issue tracker's counter and is spent whether or not the effort is ever proposed;
+ one never proposed is withdrawn after a week. Never pick a number by hand.
+3. **Propose it.** `knowledge-base.propose` with `kind: research`, the `number` reserve gave, a one-line
+ `title`, `status: active`, `date` (the day it began), and `sections`: its documents in order, each a
+ `name` and its markdown `text`. The first is `00-overview.md`: **what** is being investigated, **why**,
+ and **what it touches**. Name what it touches as `links` of kind `cites` (`"0276"`, `"research 045"`,
+ `"issue 365"`, or a design's path), not only in prose.
+4. **Do the research** in further documents of the same effort: notes, option analyses, evidence, draft
+ designs. Each change is `knowledge-base.propose` again with the effort whole and `why`, one line saying
+ why this version. Every version is kept; `knowledge-base.history` lists them and gives the difference
+ between two.
- ```yaml
- ---
- status: active
- initiated: YYYY-MM-DD
- touches: [] # areas, subsystems or as-is design docs the effort bears on
- ---
- ```
-
- Then a short prose summary: **what** is being investigated, **why**, and **what it
- touches**.
-3. Do the research in further documents in the same folder — notes, option analyses, evidence,
- draft designs. Anything goes. Keep the summary in `00-overview.md` current as the effort changes
- shape.
+`propose` refuses an effort that breaks a rule that judges one record (an unknown status, a link to
+nothing, a retired word of the glossary, a daemon's line), naming every reason at once and writing nothing.
+The knowledge base then reviews the proposal itself, through a judgement that sees only the record, the
+records it links and the rules; on `changes` the effort stays proposed with the findings, and its author
+proposes it again. An effort that needs the operator's word is proposed with `needs-operator: true` and
+waits for the operator's Approve on a channel that proves who answered.
## What makes research worth reading
@@ -37,12 +46,21 @@ carries the whole lesson without naming anything.
## Do not
-- Do not put status in prose. It lives in `00-overview.md`'s frontmatter, and the prose must not restate it.
-- Do not set `became:` while the effort is open — playbook 02 sets it at closure.
+- Do not put status in prose. It is the effort's `status`, and the prose must not restate it.
+- Do not give a `became` link while the effort is open — playbook 02 gives it at closure.
- Do not write into `03-DESIGN` from an open effort.
+- Do not write an effort, or a copy of one, into this repository: the knowledge base is the only place one
+ is read, and `records.py` refuses a new path under `01-RESEARCH/`.
## Closing
An effort never just stops. It closes through playbook [02](02-graduation.md) as `graduated`
-or `abandoned`, always with `became:` pointing at what it turned into. Nothing is deleted —
+or `abandoned`, always with a `became` link to what it turned into. Nothing is deleted —
what was rejected, and why, is the more expensive half to rediscover.
+
+## How it is checked
+
+The rules on an effort are the knowledge base's: `propose` refuses one that breaks a rule that judges one
+record (the module's table test holds one case per rule), and the rules that judge the whole record set run
+after every acceptance and raise a condition when one fails. That this repository holds no effort is
+`records.py`'s `no-records` check, on every pull request.
diff --git a/00-META/process/02-graduation.md b/00-META/process/02-graduation.md
index 18cbc8cb..e050823f 100644
--- a/00-META/process/02-graduation.md
+++ b/00-META/process/02-graduation.md
@@ -4,22 +4,20 @@
**Who runs it.** Anyone, with the decision recorded before the design moves.
+**Where a decision lives.** A decision record lives whole in the knowledge base, the seat `knowledge-base`,
+reached through the mesh MCP server from any node and any folder (ADR 0290): its number, title, topic,
+status, date, deciders, its clauses, its typed links, its reasoning and every version. This repository holds
+none, and keeps no copy. What was decided is read with `knowledge-base.ask` (a question in plain words),
+`knowledge-base.get` (one record by number; `reasoning: true` for its why) and `knowledge-base.holds` (the
+clauses in force for a topic or a record).
+
## Graduating research
1. **Check it against GENESIS.** An effort graduates only if its conclusion is traceable to
[`mission.md`](../mission.md), [`context.md`](../context.md) and
[`effect.md`](../effect.md). If it conflicts, either the effort is wrong or GENESIS is —
say which, in writing, before proceeding.
-2. **Record the decision.** Write a record in [`02-DECISIONS/`](../../02-DECISIONS/) taking the next free
- number. Format and rules are in [`02-DECISIONS/README.md`](../../02-DECISIONS/README.md). State evidence,
- not assertion, and record the options that were rejected — that is the half worth keeping.
- **A record that puts work off** — a rename, a check, a case it does not handle — names the issue that
- tracks it and the owner who will do it, in the sentence that defers it ("tracked by issue N"), or the
- deferral is not accepted:
- a deferral nothing tracks reads as done
- ([issue 354](../../04-ISSUES/354-a-decided-rename-was-never-scheduled-and-the-operator-believed-it-live/00-report.md),
- ADR 0244's code renames). [`cycle.py`](../checks/cycle.py) holds every accepted record dated from
- 2026-10-09, and an older one corrected under the rule, to it.
+2. **Record the decision** (below, *Writing a decision record*).
3. **Write the design.** Create the document under `03-DESIGN/01-to-be/` with frontmatter:
```yaml
@@ -28,27 +26,83 @@
status: designed
code: [] # owning code repo(s); set at build handoff, empty before
updated: YYYY-MM-DD
- decisions: [02-DECISIONS/NNNN-....md]
+ decisions:
+ - "ADR NNNN"
---
```
-4. **Close the effort.** Set the effort's `00-overview.md` frontmatter to `status: graduated` and
- `became:` pointing at the design document and the decision record.
+4. **Close the effort.** `knowledge-base.propose` the effort again, whole, with `status: graduated` and a
+ `became` link to the decision record (`"NNNN"`) and to the design document (its path). An effort that
+ stops without a decision is proposed with `status: abandoned` and a `became` link to what replaced it.
+
+## Writing a decision record
+
+1. **Take a number.** `knowledge-base.reserve` with `kind: decision` and `for`: the record's title. The
+ number comes from the issue tracker's counter and is spent whether or not the record is accepted; one
+ never proposed is withdrawn after a week. Never pick a number by hand.
+2. **Propose it.** `knowledge-base.propose` with the `number`, a one-line `title` in plain language, its
+ `topic` (one of the six of the reading order, [`02-DECISIONS/README.md`](../../02-DECISIONS/README.md)),
+ `date` (when the decision was taken, not when it was written down), `deciders`, and:
+ - `clauses`: what was decided, in order, each one statement opening with a bold lead
+ (`"**What is decided.** …"`);
+ - `sections`: the reasoning, in order: `Context` (what was true, with evidence), `Considered Options`
+ (numbered, each with why it was rejected), `Consequences` (what follows, including what got harder),
+ `How it is checked` (a rule states how it is checked), `References`;
+ - `links`: `extends` for a record it builds on, `supersedes` for one it replaces whole, `changes` with
+ the clause it replaces part of (`"0276 §4"`, and `from` for the clause of this record that does it),
+ `cites` for any record, effort, issue or design it names;
+ - `homes`: the documents of this repository that cite it, such as the design's path;
+ - `needs-operator: true` when it holds only once the operator approves it.
+
+ State evidence, not assertion. *"Zero of 124 modules declare `brain` as a dependency"* outranks *"the
+ dependency rule is not followed"*. Record the options that were rejected — that is the half worth keeping.
+3. **A record that puts work off** — a rename, a check, a case it does not handle — names the issue that
+ tracks it, in the sentence that defers it ("tracked by issue N"), or the deferral is not accepted: a
+ deferral nothing tracks reads as done
+ ([issue 354](../../04-ISSUES/354-a-decided-rename-was-never-scheduled-and-the-operator-believed-it-live/00-report.md),
+ ADR 0244's code renames). `propose` refuses it (rule `deferral`).
+4. **Review and acceptance.** `propose` refuses, naming every reason at once and writing nothing, a record
+ that breaks one of the rules that judge one record: a link to nothing, a clause without a bold lead, a
+ superseded record cited alone, an unknown topic, a retired word of the glossary, a daemon's line, a
+ deferral no issue tracks. The knowledge base then reviews the proposal itself, through a judgement that
+ sees only the record, the records it links and the rules, and answers `approve` or `changes`. On
+ `changes` the record stays proposed with the findings: revise it by proposing it again, whole, with `why`.
+ On `approve` it is accepted, or, when it says `needs-operator`, it waits for the operator's Approve on a
+ channel that proves who answered. Nothing proposed is in force. `knowledge-base.get` shows a record's
+ reviews and the operator's ask when it waits for one.
+
+## Changing a decision
+
+**A record's meaning is never rewritten.** A decision is changed by a new record, and a correction of fact
+by a progressive insight.
+
+- **Reversing a decision, changing its scope, rejecting an option it accepted, or making a consequence false
+ that a later record cites** is a new record that `supersedes` the old one whole, or `changes` the clauses
+ it replaces. On its acceptance the old record or clauses become `superseded` or `changed`, and every
+ answer says by what. The test is not how large the edit looks; 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*.
+- **A progressive insight** corrects a fact a record asserted that turned out wrong while the decision it
+ supports stays right: a count taken before anyone measured, a file named that does not exist. It is
+ `knowledge-base.propose` of the accepted record with `insight`: what was found, what the record said
+ before, and what it says instead. It is a new version, marked as one, dated, and reviewed like a
+ proposal; it adds and never quietly replaces; the decision, the options weighed and the consequences
+ stand untouched. If the correction changes what was decided, it is not an insight — write the new record.
+- **Where a later record changes the mechanism an older one states** without reversing it, the later
+ record `changes` the older one's clause, so a reader who arrives at the older record by a citation sees
+ what changed it since.
## Amending an existing design
A design changes only through a decision.
-1. Write the decision record. If it reverses an earlier one, the earlier record's `status:`
- becomes `superseded-by: 02-DECISIONS/NNNN-....md` — **its reasoning is never rewritten**. If the
- earlier record is sound and only a *fact* in it went stale, that is a **progressive insight**,
- corrected in place and marked in the record rather than superseded
- ([`02-DECISIONS/README.md`](../../02-DECISIONS/README.md)).
-2. Edit the to-be design document and set `updated:` to today.
+1. Write the decision record (above). If it reverses an earlier one, it `supersedes` it.
+2. Edit the to-be design document, add the record to its `decisions:` as `"ADR NNNN"`, and set `updated:`
+ to today.
3. If the amendment came from an issue, give the issue the document path in the issue tracker:
`amended-design` on its `issue-tracker.move` to `resolved`, or `issue-tracker.link` with kind
`design` before then — never in the report's frontmatter
- ([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)).
+ (ADR 0275).
## When something ships
@@ -68,3 +122,15 @@ Implementation state is a third axis, independent of both design and decision.
- Do not edit an as-is document to describe an intention. That is what the to-be layer is for.
- Do not change a decision record's meaning. Supersede it. Correcting a fact it got wrong, while
the decision stands, is a progressive insight — marked and dated in the record, never silent.
+- Do not write a decision record, or a copy of one, into this repository: `records.py` refuses a new path
+ under `02-DECISIONS/` other than its README.
+
+## How it is checked
+
+The rules on a record are the knowledge base's: `propose` refuses one that breaks a rule that judges one
+record (its module's table test holds one case per rule), and the four rules that judge the whole record set
+(an accepted decision cited by nothing, a supersession named by one side only, a changed clause nothing
+changes, an accepted record extending a superseded one) run after every acceptance and raise a condition
+when one fails. In this repository, `records.py` checks that a design's `decisions:` entries and every
+"ADR NNNN" in the text are well formed, and that no record is written here; `index.py` checks the reading
+order's six topics, which the knowledge base reads.
diff --git a/00-META/process/03-issues.md b/00-META/process/03-issues.md
index 443e615c..4760dcc3 100644
--- a/00-META/process/03-issues.md
+++ b/00-META/process/03-issues.md
@@ -22,7 +22,7 @@ incident someone must **clear**.
## The issue tracker holds the number and the status
Every issue is opened in the **issue tracker**, the module `mesh-issues` holding the seat `issue-tracker` on the
-control-node ([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),
+control-node (ADR 0275,
[to-be 53](../../03-DESIGN/01-to-be/53-the-issue-tracker.md)). Its number comes from one counter advanced by
compare-and-set, so two authors opening at the same moment never get the same one. **A number is never taken by
reading `main` or the open pull requests** — that is how numbers collided (issue 155: two records sharing a number
@@ -73,7 +73,7 @@ import read, which every imported issue names (`imported.commit` in `issue-track
**An issue about a condition** links the condition's key at once: `issue-tracker.link` with kind `condition` and
the key `mesh-controller.conditions` gives. Every message about that condition then names the issue and where it
stands; one no issue links says "No issue tracks this yet." An agent that silences a condition because an issue
- tracks it links that issue first ([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)).
+ tracks it links that issue first (ADR 0285).
**An issue wholly in the tracker** is done at this step.
2. Investigate. **Before you start, move the issue to `diagnosing` with `by`**: who works on it and its node, as
@@ -87,14 +87,14 @@ import read, which every imported issue names (`imported.commit` in `issue-track
`take-over` saying why, which is noted on the issue. A stale worker may be taken over from without a reason.
Name the fix in `fixed-by` when it merges, with a move to `fixing` that gives `by` again; the issue stays
`fixing` until the acceptance is proven live
- ([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),
- [ADR 0281](../../02-DECISIONS/0281-an-issue-knows-whether-its-fix-is-delivered-and-a-fix-stuck-after-its-merge-is-loud.md)).
+ (ADR 0284,
+ ADR 0281).
4. Resolve. `issue-tracker.move` to `resolved`, which the tracker takes only from `fixing`, with `fixed-by`, and, if the root cause was a design gap,
run playbook [02](02-graduation.md) and give `amended-design`. **A core issue** — its `located-in`
names the controller, the node-engine, the tool runner, the SDK, or the catalogue's bus, forge or build
agent — resolves with `replay`, the id of its replay in mesh-lab's replays register, proved to fail on
the commit before the fix and pass on it, or with `replay-none` saying why none is possible
- ([ADR 0237](../../02-DECISIONS/0237-a-change-is-judged-against-the-mesh-that-runs-before-it-merges-on-the-build-seat.md));
+ (ADR 0237);
`issue-tracker.move` refuses the move without it. `wontfix` needs a `reason`.
## Rules
diff --git a/00-META/process/04-build-handoff.md b/00-META/process/04-build-handoff.md
index 8399f81e..43e32e3c 100644
--- a/00-META/process/04-build-handoff.md
+++ b/00-META/process/04-build-handoff.md
@@ -7,7 +7,8 @@
## Steps
1. **Confirm the design is settled.** Its frontmatter reads `status: designed`, and every
- claim in it traces to a record in [`02-DECISIONS/`](../../02-DECISIONS/). An open question in the text is a
+ claim in it traces to a decision record in the knowledge base (`knowledge-base.get` for each entry of
+ its `decisions:`). An open question in the text is a
reason to run playbook [01](01-research.md), not to start building around it.
2. **Name the owner.** Set `code:` in the design's frontmatter to the repositories from
[`repos.md`](../repos.md). If the repository does not exist yet, add it to `repos.md` in
@@ -26,7 +27,7 @@
- Every merge is a human checkpoint, without exception.
- What the handoff leaves for later is an issue with an owner, named in the design or the decision that
leaves it — never a sentence saying "later". The design's `code:` names who owns what is built; the issue
- names who owns what is not. [`cycle.py`](../checks/cycle.py) checks the decision record
+ names who owns what is not. the knowledge base's rule `deferral` checks the decision record when it is proposed
([issue 354](../../04-ISSUES/354-a-decided-rename-was-never-scheduled-and-the-operator-believed-it-live/00-report.md)).
- Never open a pull request unprompted. A permissions list saying it is allowed is not a
request.
diff --git a/00-META/process/05-constitution-sync.md b/00-META/process/05-constitution-sync.md
index 084e6511..cfa6a1e2 100644
--- a/00-META/process/05-constitution-sync.md
+++ b/00-META/process/05-constitution-sync.md
@@ -1,6 +1,6 @@
# Playbook 05 — Constitution sync
-Implements [ADR 0021](../../02-DECISIONS/0021-hq-is-the-source-of-the-constitution.md): HQ is
+Implements ADR 0021: HQ is
the source of the constitution, and the knowledge-base page is derived, never edited.
**Trigger.** [`how-we-build.md`](../how-we-build.md) changed a rule that the mesh enforces at
diff --git a/00-META/process/06-writing-a-module.md b/00-META/process/06-writing-a-module.md
index 5f47e4f4..724e4151 100644
--- a/00-META/process/06-writing-a-module.md
+++ b/00-META/process/06-writing-a-module.md
@@ -25,7 +25,7 @@ Three questions, answered from the machine:
## The steps
1. **Name what it provides and requires**, if anything. A name is what a consumer is coupled to,
- not the role it plays ([ADR 0027](../../02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md)):
+ not the role it plays (ADR 0027):
`postgres-database`, not `database`. Most modules provide nothing and require nothing — an
application is usually a leaf.
@@ -103,5 +103,5 @@ time and the scaffolding was not.**
there and nowhere else.
- **A failing test is kept, not skipped.** It is the reproduction.
- **Never rotate during an adoption.** Rotation is a separate act, afterwards, deliberately.
-- **A data directory is never removed by the mesh** ([ADR 0030](../../02-DECISIONS/0030-data-outlives-the-mesh-that-declared-it.md)),
+- **A data directory is never removed by the mesh** (ADR 0030),
and that protects against the mesh only — not against a disk or a mistaken command.
diff --git a/00-META/process/07-feature-branches.md b/00-META/process/07-feature-branches.md
index 3ca03872..7d9cbe29 100644
--- a/00-META/process/07-feature-branches.md
+++ b/00-META/process/07-feature-branches.md
@@ -43,7 +43,7 @@ One feature is **one branch name**, **one worktree per repo**, **one MR per repo
4. **Finish, then publish.** When the whole feature is done — every repo, tests green — push
every branch and open **one MR per touched repo**, together.
5. **Merge promptly, once approved.** Every merge into `main` is notified and approved
- ([ADR 0023](../../02-DECISIONS/0023-approval-is-the-checkpoint.md)); once it is, merge —
+ (ADR 0023); once it is, merge —
do not leave it sitting. The branch is deleted on merge.
6. **Leave nothing behind.** After the MRs merge, no `feat/` branch and no `.work/`
worktree survive.
diff --git a/00-META/repos.md b/00-META/repos.md
index 7aa319d6..e503e45a 100644
--- a/00-META/repos.md
+++ b/00-META/repos.md
@@ -15,30 +15,30 @@ and a forge address is an operational detail (see [`README`](../README.md)).
| Repository | Owns |
|---|---|
| `hal` | The monorepo — the node runtime, the module catalogue, the delivery machinery, and the bootstrap scripts. Every core module lives here. |
-| `hq` | This repository, under the company organisation — mission, research, design, decisions, issue diagnosis. Company-scoped ([ADR 0019](../02-DECISIONS/0019-how-this-repository-works.md)); the mesh is its first product. The source of truth for *why*. Carries no implementation. |
+| `hq` | This repository, under the company organisation — mission, research, design, decisions, issue diagnosis. Company-scoped (ADR 0019); the mesh is its first product. The source of truth for *why*. Carries no implementation. |
| *(one per application)* | Every standalone application, site or side-project gets its own repository, with `module.yml` at the root. Registered with the mesh as a build source; built and deployed by the same pipeline as anything in the monorepo. |
| `migration` | **Private.** The record of one installation replacing the predecessor mesh with this one: the runbook, a dated log of every step and what it cost, the per-service data procedures, the readiness checks, and the scripts. Private because it is the opposite of this repository in every way that matters — it names machines, addresses, ports and paths, because a procedure that cannot be followed is not one. Where hq asks *what did we decide and why*, that repository answers *what happened on the machines, in what order, and what to do next*. Its `HANDOFF.md` is where somebody picking the work up starts. |
## What the mesh becomes
-[ADR 0019](../02-DECISIONS/0019-how-this-repository-works.md) records the repositories the
+ADR 0019 records the repositories the
monorepo decomposes into. **`mesh-host`, `mesh-controller`, `mesh-catalog`, `mesh-lab`, `mesh-sdk` and `mesh-tools` exist
-so far** — the lab was built first ([ADR 0016](../02-DECISIONS/0016-the-lab.md)). The tiered
+so far** — the lab was built first (ADR 0016). The tiered
decomposition below is the planned shape; the repositories built to date do not map onto it
one-for-one — `mesh-catalog`, `mesh-sdk` and `mesh-tools` exist where the table names
`mesh-foundation` and `mesh-surfaces`, and reconciling the two is itself still ahead.
| Repository | Tier | Holds |
|---|---|---|
-| `mesh-host` | 0 | **exists.** The node-engine — one statically linked binary, requiring nothing present ([ADR 0005](../02-DECISIONS/0005-the-node-host.md)) |
+| `mesh-host` | 0 | **exists.** The node-engine — one statically linked binary, requiring nothing present (ADR 0005) |
| `mesh-foundation` | 1 | the four pinned services, as declarations |
-| `mesh-controller` | 2 | **exists.** The controller and its contexts — one of seven built ([ADR 0006](../02-DECISIONS/0006-the-substrate-and-the-control-plane.md)) |
+| `mesh-controller` | 2 | **exists.** The controller and its contexts — one of seven built (ADR 0006) |
| `mesh-surfaces` | 3 | tools, web, cli |
-| `mesh-sdk` | — | the stable spine modules build against — the tool-serving harness, the messaging/event framework, the contracts and core primitives. Holds nothing per-module and nothing volatile ([ADR 0039](../02-DECISIONS/0039-what-the-sdk-holds-and-refuses.md)). |
+| `mesh-sdk` | — | the stable spine modules build against — the tool-serving harness, the messaging/event framework, the contracts and core primitives. Holds nothing per-module and nothing volatile (ADR 0039). |
| `mesh-lab` | — | **exists.** The lab — scenario lifecycle, networking, placement. Ships to nobody; runs on a workstation. |
Tier 4's shape is open, and deliberately so: see ADR 0019 and
-[research 005](../01-RESEARCH/005-domain-grouping/00-overview.md).
+research 005.
## What lives where inside the monorepo
@@ -56,7 +56,7 @@ Named by role, because the layout is itself part of the as-is design — see
## Why applications do not live in the monorepo
A standalone application in the monorepo is a convention violation, and reviewers reject it.
-The reasoning is recorded in [`02-DECISIONS/0010`](../02-DECISIONS/0015-applications-live-in-their-own-repository.md):
+The reasoning is recorded in ADR 0015:
the mesh installs, provisions for, and ships an application through exactly the same machinery
whether or not its source sits beside the mesh's own — so co-location buys nothing and costs
the monorepo's review cadence.
@@ -67,7 +67,7 @@ Each module is a standalone package that consumes its dependencies from the priv
not from a sibling directory. The workspace was removed after it caused build-versus-development
divergence — a workspace member importing another resolved to local unbuilt source in the
pipeline and to a published version in development. Recorded in
-[`02-DECISIONS/0007`](../02-DECISIONS/0014-no-npm-workspace.md).
+ADR 0014.
Consequence, and it is a real one: a cross-package change is two steps — publish, then consume
— and a repository-wide `npm install` does not exist.
diff --git a/01-RESEARCH/001-module-domain-decomposition/00-overview.md b/01-RESEARCH/001-module-domain-decomposition/00-overview.md
deleted file mode 100644
index 0be76a0a..00000000
--- a/01-RESEARCH/001-module-domain-decomposition/00-overview.md
+++ /dev/null
@@ -1,87 +0,0 @@
----
-status: graduated
-initiated: 2026-08-22
-touches: [03-DESIGN/00-as-is/02-modules-and-manifests.md, 03-DESIGN/00-as-is/10-module-catalogue.md, 03-DESIGN/01-to-be/00-work-breakdown.md]
-became:
- - 02-DECISIONS/0001-mesh-brokers-nodes-host-agents-think.md
- - 02-DECISIONS/0009-modules-and-the-graph.md
- - 02-DECISIONS/0037-where-a-module-lives.md
- - 02-DECISIONS/0039-what-the-sdk-holds-and-refuses.md
- - 02-DECISIONS/0173-the-operators-machine-is-the-meshs-and-a-module-is-what-it-declares.md
- - 02-DECISIONS/0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md
- - 02-DECISIONS/0244-the-mesh-is-described-in-domains-and-one-word-names-one-thing.md
----
-
-# 001 — Module domain decomposition
-
-- **Initiated by:** jochen, 2026-08-22
-- **Areas touched:** every `hal/*` and `noxflow/*` module; the pipeline's dependency
- graph; the knowledge base; agent identity and credentials.
-
-## Summary
-
-HAL has 124 modules. That number is not a maintenance problem in itself — it is the
-**symptom of missing bounded contexts**. Modules are split not because they model
-different domains, but because splitting is the only lever the platform offers:
-
-- no way to run one daemon on one node without making it a module
- (`hal/claude-licences` — one daemon, single-node)
-- no way to expose two of a kind from one module
-- no namespace separating the mesh from the software it runs
-
-This effort establishes the **current state**, the **ideal state**, and the sequence
-between them.
-
-## Trigger
-
-A night of debugging that produced four fixes and one conclusion. Every fault was a
-boundary fault:
-
-- Per-agent Claude credentials had to be written by the *noxflow runtime*, because
- `agents.claude_account` is in noxflow's database — even though agent identity is a
- mesh concept and node identity already lives in the mesh registry.
-- Whether `hal/brain` may depend on noxflow took three attempts to answer, twice
- wrongly, because the ownership boundary was never stated.
-- Authoritative documentation existed in `mesh_docs` and was not found, while a
- proposal in repo markdown was invisible to search entirely.
-
-## Decisions taken (2026-08-22)
-
-| Question | Decision |
-|---|---|
-| What should noxflow become? | Decompose into `hal/*` modules; noxflow returns to tasks/workflows |
-| Who owns agent identity? | `hal/agents` — a mesh concept, alongside nodes |
-| Where do third-party apps live? | Out of this repo. They run *on* the mesh; they are not *of* it |
-| Knowledge structure | Modelled on `papa-hq`; implementation choice left open |
-
-## Open questions
-
-Tracked in [`analysis.md`](analysis.md) under "Open questions".
-
-## Deliberately not decided
-
-Recorded so they are not mistaken for oversights. Each was open, and each came out of
-[ADR 0001](../../02-DECISIONS/0001-mesh-brokers-nodes-host-agents-think.md); how each was answered
-is under *What it became* below.
-
-| Question | Status |
-|---|---|
-| `hal/scheduler` — infrastructure, or part of the work context. | Open. |
-| Which context owns the executor. | Open. |
-| Catalogue destination — one repository or many. | Open. Phase 4. |
-| What the shared library keeps after extraction. | Open. Phase 3. |
-| Where human agent modality is recorded — which user, on which node, a human agent acts as. | Open. Required by the model; not yet stored. |
-| Which domains the modules outside the platform core group into. | Open, from [ADR 0009](../../02-DECISIONS/0009-modules-and-the-graph.md), which settles the principle and deliberately not the list. |
-
-## What it became
-
-*Closed 2026-10-10.* Every question left open was answered by a later record, or stopped applying.
-
-- **The scheduler and the executor** were questions about two of the predecessor's modules. The predecessor is
- retired and the operator's machines are the mesh's ([ADR 0173](../../02-DECISIONS/0173-the-operators-machine-is-the-meshs-and-a-module-is-what-it-declares.md)), so neither module exists to place.
-- **One catalogue repository or many**: [ADR 0037](../../02-DECISIONS/0037-where-a-module-lives.md) — the mesh's own parts live in the repositories that
- build them, software the world made is described in one catalogue, and an application carries its
- description at its own root.
-- **What the shared library keeps**: [ADR 0039](../../02-DECISIONS/0039-what-the-sdk-holds-and-refuses.md) — the stable spine, and never a module's own client or tools.
-- **Which user, on which node, a person acts as**: [ADR 0181](../../02-DECISIONS/0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md) — the operator account is a fact of the node.
-- **Which domains the modules group into**: [ADR 0009](../../02-DECISIONS/0009-modules-and-the-graph.md) settled the principle and [ADR 0244](../../02-DECISIONS/0244-the-mesh-is-described-in-domains-and-one-word-names-one-thing.md) the domains.
diff --git a/01-RESEARCH/001-module-domain-decomposition/analysis.md b/01-RESEARCH/001-module-domain-decomposition/analysis.md
deleted file mode 100644
index 8050778d..00000000
--- a/01-RESEARCH/001-module-domain-decomposition/analysis.md
+++ /dev/null
@@ -1,242 +0,0 @@
----
-effort: 001-module-domain-decomposition
-updated: 2026-08-22
----
-
-# Current state → ideal state
-
-## 1. The count is a symptom
-
-124 modules. The number is not the problem; the reason for it is. Modules are split
-because splitting is the **only granularity lever the platform offers**:
-
-| symptom | module | why it exists |
-|---|---|---|
-| one daemon must run on one node | `hal/claude-licences` | no per-node feature opt-in |
-| a library must not drag a 450 MB dep | `hal/claude` | no way to expose two npm packages |
-| a domain needs a daemon *and* a library | `hal/claude-code` + `hal/claude` | one feature of each type per module |
-| a handful of SDK verbs need a home | `infra` (10 lines) | tools must belong to *some* module |
-
-A feature is a **handler type**, discovered by directory presence, singleton per module
-(`FEATURE_HANDLERS` in `modules/hal/sdk/src/feature-handlers/index.ts`). So "one more
-deployable unit" always means "one more module".
-
-Splitting also fragments domains. Claude licence logic sits in three modules —
-resolution in `hal/claude`, health in `hal/claude-code`, refresh in
-`hal/claude-licences` — so following one token means reading three.
-
-## 2. Two things are conflated in one repo
-
-`noxflow` is 45 tables. Its original purpose — a HAL-native Jira — is 7 of them:
-
-| domain | tables |
-|---|---:|
-| tasks (the original concept) | 7 |
-| agents / HR | 13 |
-| knowledge (a Confluence) | 8 |
-| meetings | 7 |
-| scheduling / ops | 2 |
-| other | 8 |
-
-Agents alone are nearly double the concept the module was built for. This is the direct
-cause of concrete faults: per-agent Claude credentials had to be written by the noxflow
-runtime purely because `agents.claude_account` lives in noxflow's database, while node
-identity — the same kind of fact — lives in the mesh registry.
-
-Separately, 91 of the 124 modules are third-party software (`plex`, `sonarr`,
-`firefox`, `postgres`). These **run on the mesh; they are not part of it**. They belong
-outside this repository. The pipeline and provisioning system are deliberately generic,
-so HAL's own modules dogfood exactly what external modules use — that property is what
-makes the separation safe.
-
-## 3. Knowledge is split three ways
-
-| store | owner | size | search |
-|---|---|---:|---|
-| `mesh_docs` (+ revisions) | `hal/hippocampus`, in the mesh DB | 144 docs, 73 touched in 30d | yes |
-| `knowledge_*` (8 tables) | `noxflow/runtime`, in the noxflow DB | 20 pages | yes |
-| repo markdown | git | `VISION.md`, `FLAVOR-PROPOSAL.md`, `docs/` | **no** |
-
-The librarian is split across two of them: `librarian_file_document` writes via
-hippocampus, `librarian_ask` and `librarian_retrieve` read via noxflow. Filing and
-retrieval, different modules, different databases.
-
-Both DB stores have revisions; only noxflow has a review workflow (`_proposals`,
-`_promotion_requests`). Neither has file access. Git has file access and history but no
-search integration. **Every piece needed already exists — none of them together.**
-
-## 4. `hal/developer` and the missing local mesh
-
-`hal/developer` (≈2,600 lines) exposes `dev_stage`, `dev_diff`, `dev_validate`,
-`dev_typecheck`, `dev_verify_flavor`, `dev_bootstrap`, `dev_build`, `dev_up`, `dev_down`,
-`dev_env`. It is the tooling that both reflects and enforces the module model, so it
-moves with any change to that model.
-
-`dev_up` builds a dev environment **for a single module**: resolves `requires:`, starts
-providers "via systemctl (same as production)", provisions workspace-namespaced
-databases. It borrows the host node's services.
-
-**There is no way to run a mesh locally.** Consequently anything that only manifests
-*between* nodes — credential rotation reaching a running session, provision credentials
-fanning out, cascade ordering, artifact packaging — is discoverable only in production.
-Every fault fixed on 2026-08-22 was of that kind.
-
-A containerised mesh (N nodes, a broker, a registry, a coordinator) is therefore not a
-convenience. It is the precondition for a refactor of this size being verifiable at all.
-
-## 5. `hal/sdk` is where domains go to hide
-
-155 files, 34,636 lines. It contains code from essentially every bounded context:
-
-| file | domain it belongs to |
-|---|---|
-| `installer-core.ts` (1,495) | runtime |
-| `tools/provisions.ts` (1,311) | provisioning |
-| `tools/deploy-service.ts` (1,081) | deploy |
-| `workflow-engine.ts` (970), `task-commands.ts` (588) | **work** — noxflow's domain, inside HAL's SDK |
-| `artifact-manager.ts` (948), `build-executor.ts` (760), `feature-handlers/*` | delivery |
-| `env-generator.ts` (944) | config |
-| `amqp-client.ts` (899) | comms |
-| `meshware.ts` (799) | runtime |
-| `module-registry.ts` (743) | mesh |
-| `claude-credentials.ts` | **ai** |
-
-Nothing here was misplaced carelessly. Each landed in the SDK because the SDK is the one
-package every module may import — so putting a shared type there is the way to avoid a
-circular dependency between two modules that both need it.
-
-The result is that the dependency graph is trivially acyclic and completely
-uninformative: everything depends on one 34k-line package, so a change anywhere in it
-rebuilds everything. On 2026-08-22 a three-line fix in `feature-handlers/migrations.ts`
-produced a 1,445-job, 26-minute mesh-wide cascade.
-
-The last row is from that same day, and is the clearest illustration: per-agent Claude
-config-directory helpers were added to `@hal/sdk/claude-credentials.ts` because it was
-the only place both `hal/claude` and the noxflow runtime could import from. The
-alternative — `hal/ai` owning it and noxflow depending on `hal/ai` — was unavailable
-without answering the ownership question this effort exists to answer.
-
-**Extraction is therefore blocked on the decomposition, not the reverse.** A domain
-module can only own its shared code once the domain has a module. The SDK should retain
-only what is genuinely cross-cutting: transport, manifest types, logging, process
-helpers.
-
-## 6. The crux: two notions of "agent"
-
-The original vision was **a node is an agent with thinking abilities**. Later, noxflow
-implemented actual agents — employees with skills, workspaces and tasks. Both survive,
-and they collide.
-
-The collision is visible in the data. There are **two agent rows per node**:
-
-| agent | display name | skills | bound to |
-|---|---|---|---|
-| one named after each node | the node's name | `{deploy,verify,operate}` | its node |
-| `hal-`, one per node | "HAL (\)" | `{}` | its node |
-
-One carries the **work**; the other carries only **identity** — no skills at all, existing
-to hold a licence for HAL's own sessions (`resolveHalModuleEnv`). The node-as-agent idea
-was implemented twice, half each.
-
-The same split appears in the schema: `nodes.hal_claude_account` and
-`agents.claude_account` are the same fact in two tables in two databases. What was
-documented as "three licence touchpoints" (NODE, HAL, AGENT) is **one concept modelled
-three times**.
-
-### Resolution
-
-**Nodes and agents are decoupled.** The agents module owns agents; agents run on nodes.
-A node is a place where an agent can run — that is the whole of the relationship. There
-is no resident agent, no node-owned identity, no ownership in either direction.
-
-Agents named after a node still exist, but they are **ordinary agents defined through the
-agents module** that happen to hold infra skills and be named after the node they usually run
-on. Nothing about them is structural.
-
-Two consequences follow:
-
-1. **`nodes.node_license` and `nodes.hal_claude_account` should not exist.** A node does
- not authenticate to a model provider — agents do. Both collapse into
- `agents.claude_account`, and what was documented as three licence touchpoints becomes
- one.
-2. **The pair of agent rows per node merges.** `hal-` exists only to hold a licence
- for HAL's own sessions; once licences belong to agents and any module may employ an
- agent, it has no reason to be separate from ``.
-
-What remains unaccounted for is the operator's own interactive session:
-`~/.claude/.credentials.json` on a node serves a **human at a prompt**, not an agent.
-Under this decoupling that is not a node property either — it is the operator's
-credential on whichever machine they are sitting at. Whether the operator is modelled as
-a persona (soulstream treats humans and agents as peers with identical credentials) is an
-open question, and the last remaining reason `nodes` carries a licence column today.
-
-## 7. The record, not "meetings"
-
-soulstream models work as **one signed log** in which humans and agents apply identical
-changes, with work arriving by **mention**. Memory and decisions live in that single
-auditable stream — no hidden state between components. Its stated economics,
-*"cheap awareness and expensive thinking"*, is the same principle as the budget guard:
-constant monitoring, selective deep processing.
-
-Under that model "meetings" is not a domain. A meeting is a **thread in the record**.
-Which means six of today's modules and table groups are one thing seen from different
-angles:
-
-`hal/axon` (DMs, rooms) · `hal/cortex` · `hal/synapse` · `hal/notifications` ·
-`conversations` · `meetings`
-
-A notification, in particular, is just a mention not yet read — which is why it needed
-its own table, its own id column, and its own failure mode.
-
-## 8. Ideal state
-
-The mesh repository contains only the mesh, decomposed by domain rather than by
-deployment accident:
-
-| context | what it is | absorbs today's |
-|---|---|---|
-| `hal/infra` | **the room** — nodes, registry, provisioning, pipeline, node runtime | `hal/mesh`, `hal/brain`, `hal/coordinator`, `hal/meshware`, `hal/developer`, `hal/bootstrap` |
-| `hal/agents` | **the name and the thinking** — Agent owns identity, licence, runs, memory, thoughts | noxflow agents (13 tables), `hal/thoughts` |
-| `hal/stream` | **the record** — mentions, threads, meetings, notifications | `hal/axon`, `hal/cortex`, `hal/synapse`, `hal/notifications`, meetings (7), conversations |
-| `hal/knowledge` | documents, spaces, revisions, review, search | `hal/hippocampus` + noxflow `knowledge_*` |
-| `hal/work` | tasks, workflows, boards — the original noxflow | noxflow tasks (7 tables) |
-| `hal/ai` | provider integration, flavored per vendor | `hal/claude*`, `claude-code` |
-| `hal/config` | env and config distribution, secrets, PKI | `hal/env-sync`, `hal/config-sync`, `hal/secrets`, `mesh-ca` |
-| `hal/observability` | health, dashboards, logs | `hal/health`, `hal/meshboard` |
-
-Eight contexts instead of 33 platform modules, with the catalogue elsewhere.
-
-The naming corrects itself in the process: what is called "brain" today is
-infrastructure, and the word survives only as a concept an Agent owns — never again as a
-module name.
-
-This shape depends on **named features with per-node opt-in** — without it, every
-independently-deployable unit inside a context becomes a module again and the count
-returns. That mechanism is the subject of a separate ADR.
-
-## Decided since
-
-| question | decision |
-|---|---|
-| Meetings — own context or part of agents? | **Neither.** A meeting is a thread in the record; `hal/stream` absorbs it. The category was wrong. |
-| Thoughts — autonomy or agent behaviour? | `hal/agents`. Any agent can have a thought. |
-| `hal/axon` vs `hal/cortex` | `hal/cortex` is vestigial — no `daemon/` source, unit inactive on every node, description copy-pasted from axon. Messaging belongs to `hal/stream`. |
-| `hal/hypothalamus` | Orphaned build output. A `dist/` directory with no `module.yml`, so not a module at all. |
-| Who owns agent identity? | `hal/agents`. Nodes and agents are decoupled; a node is only a place an agent can run. |
-| Where do third-party modules live? | Outside this repository. They run *on* the mesh, not *of* it. |
-
-## Open questions
-
-1. **`hal/scheduler`** — infrastructure (fire an event later, belongs to `hal/mesh`) or
- part of `hal/work`?
-2. **The executor.** Pulling and routing work items is `hal/work`; spawning a session is
- `hal/agents`; providing the place to run is `hal/mesh`. It currently straddles all
- three, which is the same kind of straddle that put credential-writing in the noxflow
- runtime.
-3. **Catalogue destination** — one repository, or per-application repositories, given the
- pipeline resolves dependencies across the registry rather than the filesystem?
-4. **SDK residue** — after extraction, does `hal/sdk` keep transport (`amqp-client`), or
- does that belong to `hal/stream`? Everything imports it, which argues both ways.
-5. **Human agent modality.** ADR 0001 requires a fact the mesh does not record: which
- user, on which node, a human agent acts as. Where does it live — an attribute of the
- agent, or of the agent-node binding?
diff --git a/01-RESEARCH/002-local-mesh/00-overview.md b/01-RESEARCH/002-local-mesh/00-overview.md
deleted file mode 100644
index 26aad112..00000000
--- a/01-RESEARCH/002-local-mesh/00-overview.md
+++ /dev/null
@@ -1,59 +0,0 @@
----
-status: graduated
-initiated: 2026-08-22
-touches: [03-DESIGN/00-as-is/04-delivery.md, 03-DESIGN/00-as-is/05-runtime-and-installation.md]
-became: [03-DESIGN/01-to-be/01-end-to-end-testing.md, 02-DECISIONS/0016-the-lab.md]
----
-
-# 002 — A mesh that runs locally
-
-- **Initiated by:** jochen, 2026-08-22
-- **Areas touched:** `install.d/`, `hal/meshware`, `hal/coordinator`, `hal/brain`,
- `hal/developer` (`dev_up`), `hal/sdk` (env generation, feature handlers, artifact
- manager), `test/pipeline/`, `test/dev-mesh/`, the provisioning path in
- `modules/postgres/`.
-
-## Summary
-
-Phase 0 of [`03-DESIGN/00-work-breakdown.md`](../../03-DESIGN/01-to-be/00-work-breakdown.md) requires
-a mesh that comes up in containers, runs its own pipeline, and reproduces known faults on
-demand. Nothing else in the decomposition starts until it exists, because every fault the
-decomposition addresses was found in production — there was nowhere else to find it.
-
-This effort establishes what already runs in a container, what is welded to the host, and
-what it would take to close the gap. It does **not** choose an approach: the central
-question — how a containerised node executes a module service, when a module service is
-defined today as a systemd unit shelling to `docker compose` in `/services/` — is not
-answered by ADR 0001 and is recorded below rather than decided.
-
-## What was established
-
-- The two existing container harnesses are **neither of them a mesh**, and one of them has
- not been able to build since 2026-06-04.
-- Node identity is already portable — a single environment variable, no host handshake.
-- Four concrete host couplings block a containerised node, all with known locations.
-- All three Phase 0 fixtures are reproducible; one of them is documented in the knowledge
- base with an open root cause and is the cheapest place to start.
-
-Detail and evidence in [`analysis.md`](analysis.md).
-
-## Questions — all settled 2026-08-22
-
-**Phase 0 is a development environment**, not a fixture rig — it is what the host-borrowing
-dev tooling becomes.
-
-**The trigger is a real source-forge container**, because the webhook relay is part of what
-is under test.
-
-**A node is a system container**, promotable to a virtual machine per node. This answered
-the question the effort was stuck on, and dissolved it rather than solving it: against a
-real node with a real init, the four host couplings catalogued in `analysis.md` §3 are not
-couplings — they are how a node works. They were obstacles only to a node modelled as an
-application container.
-
-Consequently [`003-service-supervision`](../003-service-supervision/) **no longer blocks
-Phase 0**. It remains a live architecture question, on its own timeline.
-
-The remaining questions in `analysis.md` — what replaces host paths in a container, and how
-faithful the lab must be — are answered in the design: nothing replaces them, because the
-paths are real; and the divergences are enumerated rather than discovered.
diff --git a/01-RESEARCH/002-local-mesh/analysis.md b/01-RESEARCH/002-local-mesh/analysis.md
deleted file mode 100644
index c14ca147..00000000
--- a/01-RESEARCH/002-local-mesh/analysis.md
+++ /dev/null
@@ -1,263 +0,0 @@
----
-effort: 002-local-mesh
-updated: 2026-08-22
----
-
-# A mesh that runs locally — current state and obstacles
-
-Evidence for Phase 0. Every claim here is either a file location or something measured on
-2026-08-22; where a claim was checked and found false, that is recorded too.
-
----
-
-## 1. What exists today, and why neither is a mesh
-
-### `test/pipeline/` — the only containerised HAL node, and it is dead
-
-This harness builds `@hal/sdk` and the `hal/brain` daemon into a `node:23-alpine` image,
-compiles `hal/coordinator`'s tools against it, and runs `brainstem.js` with
-`HAL_MODE=daemon` against a containerised postgres and LavinMQ
-(`test/pipeline/Dockerfile`, `test/pipeline/docker-compose.yml`). It proves the valuable
-thing: **a node runtime needs no systemd, no `/services/`, and no nvm** — environment
-variables and a seeded `nodes` row are sufficient.
-
-It also cannot build. Measured 2026-08-22:
-
-```
-Step 7/16 : RUN npm run build -w modules/hal/sdk
-npm error No workspaces found:
-npm error --workspace=modules/hal/sdk
-```
-
-The npm workspace it depends on was removed on 2026-06-04 (`21ef4a4e`, "kill npm
-workspace"), and the root `package.json` now carries a comment explaining why it will not
-come back. The harness has not been touched since before that commit.
-
-So the repository's only end-to-end pipeline test has been unrunnable for two and a half
-months and nothing reported it — which is the same shape as the faults Phase 0 exists to
-catch. **A test nobody runs is indistinguishable from a test that passes.**
-
-### `test/dev-mesh/` — a work plane, not a control plane
-
-Four `runtime-*` services, a noxflow API, a meshboard and a shared postgres + LavinMQ on
-one network (`test/dev-mesh/docker-compose.yml`). It is genuinely useful and the topology
-is a good precedent: one network, per-service `HAL_NODE`, `EXECUTOR_STUB=1` so dispatch and
-run lifecycle are real while the model call is faked.
-
-It is the wrong layer for Phase 0, on three counts:
-
-| | dev-mesh does | Phase 0 needs |
-|---|---|---|
-| state | restores `pg_dump`s (`test/dev-mesh/init.sh`) | schema built by **migrations**, or fixture 3 is untestable by construction |
-| code | bind-mounts pre-built `dist/` from the host | artifacts **built and delivered** by the pipeline |
-| scope | noxflow runtime, API, meshboard | `hal/coordinator`, `hal/meshware`, MinIO, provisioning |
-
-No meshware, no coordinator, no artifact store, no provisioning, no build. It exercises
-what the mesh *runs*; Phase 0 needs what the mesh *is*.
-
-### `dev_up` — confirmed host-coupled
-
-The claim in the work breakdown holds. `startDevProvider()` starts providers through the
-host's user systemd instance and reads credentials from a host path
-(`modules/hal/developer/tools/dev-env.ts:146-178`):
-
-```ts
-const unitName = `hal-module@${provider}.service`;
-execSync(`systemctl --user start ${unitName}`, { timeout: 60_000, stdio: "pipe" });
-const creds = resolveRunningProviderCreds(provider); // reads /services//.env
-```
-
-It borrows the host. There is no mesh to stand up.
-
----
-
-## 2. What is already portable
-
-- **Node identity is one environment variable.** `resolveNodeName()` is
- `process.env.HAL_NODE || hostname()`, lowercased (`modules/hal/sdk/src/mesh-config.ts:215`).
- No file, no registration handshake, no host coupling.
-- **Registration is a plain upsert.** `mesh_node_register` inserts into `nodes`
- (`modules/hal/mesh/tools/index.ts:490-540`); mandatory fields are `name` and `user_name`.
-- **The daemons are AMQP loops.** `hal/meshware` consumes `cmd.feature.{build,install,configure,start}`
- (`modules/hal/meshware/daemon/src/cerebellum.ts:780-857`); `hal/coordinator` consumes
- `event.gitea.push` and drives the cascade. Both are configured entirely by environment.
-- **`hal/brain` in daemon mode already runs in a plain node image**, per `test/pipeline/`.
-
----
-
-## 3. The obstacles, located
-
-Four couplings stand between the daemons and a container. All are in code, none are
-mysterious.
-
-| # | coupling | location |
-|---|---|---|
-| 1 | meshware restarts itself via the host: `execFileSync("systemctl", ["--user","restart","hal-meshware.service"])` | `modules/hal/meshware/daemon/src/cerebellum.ts:826` |
-| 2 | meshware restarts the broker via a hardcoded host path: `execFileSync("docker",["compose","-f","/services/lavinmq/docker-compose.yml","restart"])` | `cerebellum.ts:843-844` |
-| 3 | **a module service *is* a systemd unit** — `hal-module@.service` runs `docker compose --project-directory /services/%i` | `modules/hal/meshware/systemd/hal-module@.service:10-11` |
-| 4 | env generation writes `homedir()`-relative host paths — `~/.config/hal/env`, `~/.config/hal/modules/*.env`, `/services/*/.env` | `modules/hal/sdk/src/env-generator.ts:188-223`, `:538-568`, `:584-606` |
-
-1, 2 and 4 are small: a guard and a configurable root. **3 is the architectural one**, and
-it is the first open question below — in a container, "systemd unit that runs docker
-compose" has no natural translation.
-
-The bootstrap scripts add their own host assumptions — nvm for node resolution, `sudo
-mkdir -p /services && chown`, and a `~/dotfiles` repository that is not in this repo
-(`install.d/adopt.sh:165-170`) — but Phase 0 does not have to run them. A container image
-can be built the way `test/pipeline/Dockerfile` builds one, bypassing the bootstrap path
-entirely. That is a deliberate divergence to record: **the local mesh would not test the
-bootstrap**, only the running mesh.
-
----
-
-## 4. External services, and whether they can be local
-
-| service | used for | local substitute |
-|---|---|---|
-| PostgreSQL | registry + pipeline state | yes — already the pattern in both harnesses |
-| LavinMQ | all coordinator↔meshware messaging | yes — `cloudamqp/lavinmq`, already used |
-| MinIO | per-feature artifact tarballs, `modules/{name}/{version}/{feature}.tar.gz` (`modules/hal/sdk/src/artifact-manager.ts:21,65`) | yes — `minio/minio`, needs only `REGISTRY_MINIO_*` |
-| Gitea | source, push webhook, **and** the `@hal/*` npm registry | container exists, but heavy; see open question 2 |
-| Docker registry | `docker push` for modules declaring `docker:` (`build-executor.ts:195-231`) | `registry:2`, or avoid modules that need it |
-| Traefik | writes routing config at install; does not gate success | omit |
-
-Only Gitea is awkward, and only because it carries two roles at once.
-
----
-
-## 5. The fixtures
-
-Phase 0.5 asks for at least one reproducible known fault. All three are reachable; they
-differ sharply in cost.
-
-### Fixture B — a provider deploy rotates a shared credential without fanning out
-
-**Cheapest, best understood, and the root cause is still open.** Documented at
-`troubleshooting/provision-adoption-rotates-live-credential`. The mechanism is two lines:
-
-```ts
-async provision(project, username, options) {
- const password = generatePassword(); // ALWAYS a fresh password
-```
-
-and, in the adoption branch of `provisionDatabase`, an `ALTER ROLE ... WITH PASSWORD` that
-rotates the live secret while updating only the provider's own `mesh_provisions` row
-(`modules/postgres/tools/index.ts`). Every consumer sharing that role keeps a stale
-password and fails permanently; the provider recovers alone, and that asymmetry is the
-tell.
-
-Reproduction needs one provider node and two consumer nodes — the minimum interesting
-mesh. It re-fires on every provider deploy, so it does not need to be provoked, only
-observed. Confirmation is a timestamp comparison, not a hash: the stored values are
-`enc:v1:` with a random IV, so identical plaintexts hash differently.
-
-Measured in production 2026-08-22: three nodes failing since 2026-08-20 14:15, 399 errors
-each; the provider failed for 22 minutes and recovered by itself.
-
-### Fixture C — a migration ships nothing while the pipeline reports success
-
-Three independent silent-skip points, any one of which produces it:
-
-1. **Not-applicable and succeeded are the same status.** `MigrationsHandler.detect()` is
- `existsSync(join(moduleDir, "migrations"))` against the freshly cloned workspace
- (`modules/hal/sdk/src/feature-handlers/migrations.ts:32-41`). If the directory is not
- there — uncommitted, ignored, or a `module_path` that does not line up — the build
- reports `success` (`cerebellum.ts:634-638`).
-2. **Upload failure is a warning.** `uploadFeatureArtifact()` returns `{sha256: ""}` with a
- `warn` when none of the listed files exist (`artifact-manager.ts:41-44`), and callers
- catch and warn rather than throw (`cerebellum.ts:672-673`). The symmetric download
- failure on the target node is also non-fatal (`artifact-manager.ts:93-98`,
- `cerebellum.ts:426-431`, commented "Non-fatal: feature may work without artifact").
-3. **Missing packaged files are skipped in silence.** `addFileEntries()` does
- `if (!existsSync(abs)) continue;` (`module-builder-core.ts:134-135`) — an explicit
- `package:` entry that is not on disk simply is not in the tarball.
-
-This is the same class as `troubleshooting/flavors-never-packaged` (`flavors/` was never
-staged, so a node kept its first copy forever) and is catalogued in
-`troubleshooting/deploy-reports-transport-not-effect`, which counted **0 of 119 modules
-implementing `verify`** — the stage that exists, is dispatched, has a working handler, and
-would turn every one of these into a red pipeline.
-
-### Fixture A — a credential rotation does not reach a running session
-
-The most valuable and the most work: it needs a *running session* to rotate underneath,
-which means the local mesh must be able to start one. The mechanism is understood — an
-`EnvironmentFile` is read once and `process.env` is a snapshot — and it is why credentials
-became files that get re-read. Defer it behind B and C.
-
-**Recommendation:** B first. It needs three nodes and a provision, no build and no session,
-and it is the one whose root cause is still open — so reproducing it locally has value
-beyond proving the harness.
-
----
-
-## 6. A claim that was checked and found false
-
-The survey behind this document suspected that the live AMQP pipeline never writes
-`deployments` or `node_modules.installed_version`, because the code that does so sits in
-`installer-core.ts:1045-1126` on what is commented as the "CLI path".
-
-Measured against production, 2026-08-22 17:30 UTC — deployments in the preceding 24 hours:
-
-| node | deploys | newest |
-|---|---|---|
-| 1 | 24 | 17:26:17 |
-| 2 | 27 | 17:22:22 |
-| 3 | 47 | 17:25:54 |
-| 4 | 26 | 17:22:23 |
-
-The record is live on all four nodes and minutes old. **The claim is refuted.** Recorded
-because the reasoning was plausible and someone will retrace it.
-
----
-
-## 7. Questions
-
-### Settled (jochen, 2026-08-22)
-
-**Phase 0 is a development environment, not a fixture rig.** It is built as a supported
-surface to work in daily, and it is what `dev_up` should become. The definition of done —
-"demonstrated in the local mesh" — is therefore meant literally.
-
-**The trigger is a real Gitea container.** The webhook relay is part of what is under test,
-including the changed-file enrichment that exists because the webhook truncates at 20
-commits (`modules/hal/gitea/tools/index.ts:136-195`). A synthetic `event.gitea.push` would
-skip it. Gitea also carries the `@hal/*` npm registry, so the local mesh needs it twice
-over either way.
-
-### Open — blocking
-
-1. **How does a node supervise a module service?** Today a module service is a systemd unit
- running `docker compose` against `/services/`
- (`modules/hal/meshware/systemd/hal-module@.service:10-11`), which has no direct
- translation inside a container. The question raised in response is the better one, and
- is broader than Phase 0: **what would it cost to stop using systemd altogether and have
- the mesh supervise its own services?** That is a mesh-level architectural question, not
- a local-mesh implementation detail — if the answer is that the mesh should own
- supervision, Phase 0 should not build a container-only workaround first. Under
- investigation; findings will land in [`003-service-supervision`](../003-service-supervision/)
- and, if it goes ahead, a decision record of its own.
-
-2. **What replaces `~/.config/hal/env` and `/services/` inside a container?** A configurable
- root keeps one code path; container-specific targets keep the host paths untouched. This
- decides whether env generation gets a seam or a conditional.
-
-3. **How faithful must the local mesh be to be trusted?** It will not run the bootstrap
- scripts, and it will run one OS where the real mesh is heterogeneous by design
- (`00-META/context.md`). Stating the divergence up front is what stops "it works
- locally" from becoming its own class of silent failure. Now sharper, because a
- development environment people use daily is trusted far more than a rig — and drifting
- from production costs correspondingly more.
-
----
-
-## References
-
-- [`02-DECISIONS/0001`](../../02-DECISIONS/0001-mesh-brokers-nodes-host-agents-think.md) — the decision this
- phase unblocks
-- [`03-DESIGN/00-work-breakdown.md`](../../03-DESIGN/01-to-be/00-work-breakdown.md) — Phase 0 tasks
- and checkpoint
-- `troubleshooting/provision-adoption-rotates-live-credential` — fixture B, root cause open
-- `troubleshooting/deploy-reports-transport-not-effect` — the nine defects that shipped
- green, and the unused `verify` stage
-- `troubleshooting/flavors-never-packaged` — fixture C, previously seen in the wild
diff --git a/01-RESEARCH/003-service-supervision/00-overview.md b/01-RESEARCH/003-service-supervision/00-overview.md
deleted file mode 100644
index 3bd1dd27..00000000
--- a/01-RESEARCH/003-service-supervision/00-overview.md
+++ /dev/null
@@ -1,79 +0,0 @@
----
-status: graduated
-initiated: 2026-08-22
-touches: [03-DESIGN/00-as-is/05-runtime-and-installation.md]
-became:
- - 02-DECISIONS/0005-the-node-host.md
- - 03-DESIGN/01-to-be/05-the-node-host.md
----
-
-# 003 — Who supervises a service
-
- **No longer blocks Phase 0** (see below).
-- **Initiated by:** jochen, 2026-08-22, in response to
- [`002-local-mesh`](../002-local-mesh/analysis.md) open question 1
-- **Areas touched:** every module shipping a `systemd/` directory (14), the
- `hal-module@` template, `hal/sdk` feature handlers, `dev_up`, `log_tail` /
- `systemd_journal`, the bootstrap scripts.
-
-## The question
-
-`002` asked how a containerised node runs a module service, given that a module service is
-defined today as a systemd unit running `docker compose` against `/services/`. The response
-was the better question:
-
-> If it's possible to run systemd inside a container, that's the way to go I think. However,
-> what would the cost be to step away from systemd to run our services and set it up in a
-> different way? More hal mesh approach.
-
-This effort answers the cost half. It does not choose.
-
-## Summary of findings
-
-- **The question splits in two**, and the halves have opposite answers. Supervising
- `docker compose` stacks through systemd is largely **redundant** — 44 of 44 module compose
- files already declare a restart policy, so Docker is already the supervisor. Supervising
- HAL's **9 long-running Node daemons** is not redundant: `Restart=always` is currently the
- only thing between a crash and a dead node.
-- **The hard part is fate-sharing, not systemd.** meshware already cannot restart itself and
- carries a documented workaround for it. Any mesh-native supervisor inherits that problem
- recursively unless it sits outside the mesh's own process tree — at which point it is an
- OS-level supervisor again, just reinvented.
-- **There is a third option neither of us named**, and it is the one that also solves Phase 0:
- run HAL's own daemons as containers, making Docker the supervisor for everything. Local and
- production then have the same shape rather than a translation layer between them.
-- **It cannot be all-or-nothing**, and ADR 0001 already says why: a human agent acts through a
- shell and a desktop. Those parts are on the host by definition.
-- One incidental finding: the automatic node rescue that documentation describes **does not
- exist**. No unit declares `OnFailure=`, and nothing calls `hal-rescue.sh` on a timer.
-
-Detail and costs in [`analysis.md`](analysis.md).
-
-## What it became
-
-*Closed 2026-08-28.* The decision this effort asked for was taken — and taken without citing it,
-which is why the effort sat `active` for five days after being answered. Recorded here because
-finding that is the point of a sweep.
-
-**The third option is what the mesh adopted.** `Docker is the supervisor for everything` is
-[ADR 0005](../../02-DECISIONS/0005-the-node-host.md): the
-host is a plain process on the machine and everything above tier 0 is a container. The substrate
-bootstrap declares no service at all — it is package, container, action, container — so the
-44-of-44 restart policies this effort counted are the supervision, exactly as it argued.
-
-**Fate-sharing was the hard part, and it is solved the way this effort predicted.** It said any
-mesh-native supervisor inherits the problem *unless it sits outside the mesh's own process
-tree*. [ADR 0005](../../02-DECISIONS/0005-the-node-host.md) puts
-the launcher there: it supervises the host as a child and shares no code with it, so a host that
-cannot start is still recovered.
-
-**It is not all-or-nothing, as this effort insisted.** A human agent acts through a shell and a
-desktop, and those are on the host. So is the host itself — the one thing an init starts.
-
-## What is not closed
-
-**The automatic node rescue the documentation describes does not exist.** No unit declares
-`OnFailure=`, and nothing calls the rescue script on a timer. That is a documented behaviour
-which never happens, and it outlives this effort — filed as
-[`04-ISSUES/008`](../../04-ISSUES/008-the-documented-node-rescue-does-not-exist/00-report.md)
-rather than closed with it.
diff --git a/01-RESEARCH/003-service-supervision/analysis.md b/01-RESEARCH/003-service-supervision/analysis.md
deleted file mode 100644
index 9a5aafb8..00000000
--- a/01-RESEARCH/003-service-supervision/analysis.md
+++ /dev/null
@@ -1,230 +0,0 @@
----
-effort: 003-service-supervision
-updated: 2026-08-22
----
-
-# Who supervises a service — the cost of leaving systemd
-
-Measured 2026-08-22. Every claim is a file location or a count.
-
----
-
-## 1. What systemd actually does for HAL
-
-**14 modules** ship a `systemd/` directory. The units divide cleanly into three kinds:
-
-| kind | count | what it is |
-|---|---|---|
-| long-running daemons | 9 | `hal/brain`, `hal/meshware`, `hal/coordinator`, `hal/cortex`, `hal/env-sync`, `hal/file-share`, `hal/thoughts`, `noxflow/runtime`, `desktop_notifications` — all `Restart=always`, `RestartSec=10` |
-| scheduled one-shots | 6 timers | docker-prune, docker-registry maintenance, claude-code sessions + usage, health, mailu cert-sync |
-| the template | 1 | `hal-module@.service` — the per-module Docker lifecycle |
-
-Only `mailu` is system-scope. Everything else is a user unit under
-`~/.config/systemd/user`, auto-detected from directory presence — the module author names
-the file and that filename *is* the unit name
-(`modules/hal/sdk/src/feature-handlers/systemd.ts:31-42`).
-
-The template is the piece `002` tripped over
-(`modules/hal/meshware/systemd/hal-module@.service`):
-
-```
-ExecStart=docker compose --project-directory /services/%i --project-name %i up --remove-orphans --pull always
-ExecStop=docker compose --project-directory /services/%i --project-name %i down
-Restart=on-failure
-StartLimitIntervalSec=120
-StartLimitBurst=5
-```
-
-Systemd's roles, then: restart-on-failure, start at login, env-file loading, ordering,
-per-module lifecycle, timers, and — via journald — **the only log store the 9 Node daemons
-have** (`modules/hal/sdk/src/tools/log-tail.ts:50-53`).
-
----
-
-## 2. The question splits in two, and the halves disagree
-
-### For `docker compose` stacks, systemd is mostly redundant
-
-**44 of 44** module `docker-compose.yml` files declare a container-level `restart:` policy —
-`always` or `unless-stopped`, none missing. Docker's own daemon already restarts crashed
-containers, independently of systemd.
-
-`hal-module@.service` does not supervise the containers. It supervises the **`docker compose
-up` foreground process**. It is a second layer on top of a restart policy that already
-works. What it genuinely adds is narrower than it looks:
-
-- one uniform verb for every module type — `systemctl --user start hal-module@X` rather than
- remembering each module's compose invocation, relied on across a dozen call sites in
- `meshware.ts:124-154`, `dev-env.ts:142-163`, `installer-core.ts`, `infra.ts:166`
-- recovery when the `docker compose up --pull always` process itself dies — a failed image
- pull, not a crashed container
-- rate-limited restart (`StartLimitBurst=5`) so a broken stack does not spin
-
-That is real, but it is a convenience layer, not a safety layer. **This half could go at
-moderate cost.**
-
-### For the 9 Node daemons, systemd is load-bearing
-
-There is no alternative supervisor anywhere in the repo. No PM2, no forever, no nodemon, no
-watchdog loop — all checked, zero hits. `Restart=always` is the only thing standing between
-a crashed daemon and a dead node.
-
-**This half is the actual question.**
-
----
-
-## 3. The hard part is fate-sharing
-
-The difficulty is not systemd. It is that **a supervisor must not share fate with what it
-supervises**, and the codebase already has a scar from exactly this.
-
-meshware cannot restart itself mid-request: killing the process before it ACKs the AMQP
-message loses the message. So it defers its own restart by two seconds after closing the
-connection (`modules/hal/meshware/daemon/src/cerebellum.ts:815-828`) — a commented
-workaround for a problem that only exists because the thing being restarted is the thing
-doing the restarting.
-
-Any mesh-native supervisor inherits this recursively. Something has to be the outermost
-always-alive process, and if it is written by the mesh, the mesh must supervise it, and so
-on. The recursion only terminates at a process the mesh does not own. Today that is
-systemd. **A "more mesh" supervisor that is itself a mesh process is not a smaller problem;
-it is the same problem with a new name.**
-
-This is the strongest argument for the status quo, and it is worth stating plainly before
-looking at alternatives.
-
----
-
-## 4. The option neither of us named
-
-There is a third answer that terminates the recursion in something that is not systemd and
-not written by us: **run HAL's own daemons as containers.**
-
-Docker is already the outermost supervisor for 44 of 44 module stacks. It does not share
-fate with the mesh. It already has restart policies, backoff, and a log store that
-`log_tail` already speaks (`log-tail.ts:43-47` reads `docker logs` for Docker modules
-today). Extending it from "the things the mesh runs" to "the mesh itself" is not new
-machinery — it is applying machinery the mesh already trusts to one more case.
-
-What that buys, beyond supervision:
-
-- **Phase 0 stops being a translation.** A containerised node becomes the same shape as a
- production node, rather than a local approximation with a systemd-shaped hole in it. The
- divergence that `002` open question 3 worries about largely disappears.
-- **`/services/` and `~/.config/hal/` stop being special.** Mounts, not host paths.
-- **The GENESIS "dogfood everything" value gets easier**, not harder: the mesh's own
- components would ship and run exactly like everything else it carries.
-
-What it costs, honestly:
-
-- **journald → docker logs** for the 9 daemons. `log_tail` already handles both, but
- `systemd_journal` and the health checks that read unit state
- (`modules/hal/mesh/health.sh:34-40`, `modules/hal/health/hal-health.sh:335-395`) would
- need a container-aware path.
-- **Boot start** becomes Docker's `restart: always` plus the Docker daemon being enabled at
- boot — which is still one systemd unit, but the OS's own, not ours.
-- **A container needs the host to be reachable** for anything that touches the node itself.
- Some of these daemons exist precisely to write host files.
-
----
-
-## 5. Why it cannot be all-or-nothing — and ADR 0001 already says so
-
-Some of what runs under systemd today **cannot** be containerised, and the reason is
-already in the domain model. ADR 0001:
-
-> a non-human agent acts through a spawned session — a human agent acts through a shell or
-> desktop
-
-`hal/brain` has two modes (`modules/hal/brain/daemon/src/brainstem.ts:7-13`): a daemon mode
-that is an AMQP relay, and a **cortex mode that is an MCP server over stdio for an
-interactive Claude Code session**. The second is a human agent's modality. It runs in the
-human's shell, on the human's node, against the human's `~/.claude`. Containerising it is
-not a hard engineering problem, it is a category error.
-
-The same holds for `desktop_notifications` and everything `hal/desktop-environment` touches.
-
-So the line is not "systemd or not". It is:
-
-| | belongs where |
-|---|---|
-| mesh daemons — meshware, coordinator, env-sync, thoughts, file-share, brain **in daemon mode** | supervisable by Docker; candidates to containerise |
-| human-modality surfaces — brain **in cortex mode**, desktop notifications, desktop environment | on the host, by definition |
-| module stacks | already Docker; systemd layer is the redundant part |
-
-This split is not a compromise between the options. It is what the domain model implies,
-and it is a decent sign that the model is doing work.
-
----
-
-## 6. Options, with costs
-
-| | option | cost | what it buys |
-|---|---|---|---|
-| **A** | **Keep systemd; systemd-in-container for Phase 0** | privileged containers, heavy images, slow iteration; local mesh keeps a shape production does not have | nothing changes in production; smallest change to the mesh |
-| **B** | **Drop the `hal-module@` layer only** — let Docker's restart policies supervise stacks directly | reimplement uniform start/stop across ~12 call sites; lose rate-limited restart and pull-failure recovery | removes the redundant layer; does **not** solve Phase 0 on its own, since the daemons still need supervising |
-| **C** | **Containerise the mesh daemons; Docker supervises** | container-aware `systemd_journal`/health; host access for daemons that write host files; the human-modality surfaces stay on the host regardless | terminates the fate-sharing recursion without writing a supervisor; makes local and production the same shape; Phase 0 becomes much less of a special case |
-| **D** | **Write a mesh-native supervisor** | the fate-sharing recursion (§3), plus matching systemd's maturity — backoff, resource limits, clean SIGTERM (`noxflow/runtime` already depends on `TimeoutStopSec=60`) — on machines that are somebody's daily driver | most "mesh"; least justified by the evidence |
-
-**D is the option the phrasing "more hal mesh approach" points at, and the evidence argues
-against it.** Supervision is not a domain concern the mesh is better placed to solve than
-the OS; the mesh's distinguishing feature is brokering capabilities, not restarting
-processes. C gets the benefit D is reaching for — the mesh not depending on host-specific
-init — without the recursion.
-
-**C and B compose.** C is the one that pays for Phase 0.
-
-Worth noting what is *not* in this table: the dozens of `systemctl` call sites that
-configure the **host OS's own** units — NetworkManager, resolved, oomd, zram, docker.service,
-fail2ban, sshd, zfs, across the vendor-firmware and power modules, `wireguard`, `sshd`, `zfs`
-and others. Those are not HAL supervising itself; they are HAL configuring an Arch box. They
-are out of scope for every option above and do not go away under any of them.
-
----
-
-## 7. An incidental finding
-
-Documentation describes an automatic node rescue: `hal-rescue.sh:20` states it is "triggered
-automatically by `hal-health.timer` when hal-meshware is failed".
-
-**It is not.** `hal-health.sh` contains no call to `hal-rescue.sh` (checked, zero matches),
-and **no unit in the repository declares `OnFailure=`** (checked, zero matches). The only
-real triggers are the manual `rescue_node` tool
-(`modules/hal/sdk/src/tools/deploy-rescue.ts:77-92`) and running `install.d/rescue.sh` by
-hand.
-
-This is worth recording for two reasons. It weakens any argument that systemd-adjacent
-self-healing is already wired — it is not. And it is another instance of the pattern this
-whole refactor is about: **a documented mechanism that does not exist, believed because it
-was written down.** `00-META/how-we-build.md` calls this out as a rule; here it is again,
-found by grep.
-
----
-
-## 8. Open question
-
-**Which supervision model does the mesh adopt?** A, B, C, D or a combination.
-
-**This no longer gates Phase 0.** When this was written, the local mesh was assumed to be
-built from application containers, which forced the question — there is no natural way to
-run an init system inside one. The decision of 2026-08-22 to build development nodes as
-**system containers** (see [`03-DESIGN/01-end-to-end-testing.md`](../../03-DESIGN/01-to-be/01-end-to-end-testing.md))
-removes that pressure entirely: a system container runs a real init, so the existing model
-works unmodified and the lab needs no answer here to exist.
-
-What remains is the question on its own merits, which is worth keeping open because the
-evidence above still holds: option B removes a layer that 44 of 44 module stacks have already
-made redundant, and option C would let the mesh stop depending on host-specific init. Neither
-is urgent. Both are now cheap to *try*, because there is somewhere to try them.
-
-Option D — a mesh-written supervisor — remains the one the evidence argues against, for the
-fate-sharing reason in §3.
-
-## References
-
-- [`002-local-mesh`](../002-local-mesh/analysis.md) — the effort this came out of
-- [`02-DECISIONS/0001`](../../02-DECISIONS/0001-mesh-brokers-nodes-host-agents-think.md) — agent modality, which
- decides what cannot leave the host
-- `modules/hal/meshware/daemon/src/cerebellum.ts:815-828` — the self-restart workaround
-- `modules/hal/meshware/systemd/hal-module@.service` — the per-module Docker lifecycle
-- `modules/hal/sdk/src/feature-handlers/systemd.ts` — detection, install, start
diff --git a/01-RESEARCH/004-lab-network/00-overview.md b/01-RESEARCH/004-lab-network/00-overview.md
deleted file mode 100644
index 18373348..00000000
--- a/01-RESEARCH/004-lab-network/00-overview.md
+++ /dev/null
@@ -1,67 +0,0 @@
----
-status: graduated
-initiated: 2026-08-22
-touches: [03-DESIGN/00-as-is/01-mesh-and-transport.md, 03-DESIGN/01-to-be/01-end-to-end-testing.md]
-became:
- - 02-DECISIONS/0016-the-lab.md
- - 02-DECISIONS/0016-the-lab.md
- - 02-DECISIONS/0016-the-lab.md
- - 03-DESIGN/01-to-be/02-scenario-declaration.md
- - 03-DESIGN/01-to-be/01-end-to-end-testing.md
----
-
-# 004 — Reproducing the mesh network in a lab
-
-- **Initiated by:** jochen, 2026-08-22 — *"the most difficult part of our VM setup will be
- the networking part"*
-- **Areas touched:** `modules/wireguard`, `modules/dnsmasq-app`, `modules/traefik`,
- `modules/mesh-ca`, `node_accessors`, `nodes.site` / `nodes.underlay_addr`.
-
-## Summary
-
-The network is **entirely generated from mesh-DB rows by module hooks**. `install.d` performs
-no network configuration whatsoever — no WireGuard, no DNS, no firewall. That makes a faithful
-lab primarily a *data* problem rather than a networking problem, and means the lab exercises
-the real code path instead of a reimplementation of it.
-
-One constraint decides whether the lab works at all: the WireGuard endpoint rule tests the
-underlay address against an RFC1918 regex to decide reachability. **A simulated public segment
-addressed from RFC1918 space silently prevents the mesh from forming** — no endpoint is written
-for the hub, so nothing can ever initiate. The simulated public segment must therefore use
-TEST-NET-3 (`203.0.113.0/24`).
-
-With that one substitution the lab reproduces the production topology exactly, including the
-case that is hardest to get right: a node that is publicly *named* but sits behind NAT, whose
-endpoint the hub can only learn from a handshake.
-
-Detail in [`analysis.md`](analysis.md).
-
-## Settled
-
-**The lab issues its own certificates.** Public names are certified by an ACME server on the
-lab's wan segment; `.internal` names keep the mesh CA. The lab preserves production's two-CA
-split rather than collapsing it, because a single-CA lab would hide any bug living in that
-split. It also makes the router's port forward load-bearing — HTTP-01 must reach the
-published-but-NATed node on port 80, so a broken forward becomes a reproducible certificate
-failure instead of a mystery.
-
-Requires one change: `caServer` is not set on the reverse proxy today, so it defaults to the
-public authority's **production** endpoint. It must become configurable, defaulting to
-production so real nodes are unaffected.
-
-## Open
-
-*Closed 2026-08-25.* The lab is stood up. The topology this effort described raises, and the
-substitution it turned on — a simulated public segment addressed from documentation space
-rather than RFC1918 — is enforced by the declaration validator before anything is raised
-rather than left as a thing to remember.
-
-The certificate conclusion above is carried by
-[`01-end-to-end-testing.md`](../../03-DESIGN/01-to-be/01-end-to-end-testing.md), which
-specifies the lab's own ACME issuer on the public segment. It is **designed and not built** —
-implementation state is a third axis, and the effort graduates on its conclusions, not on
-their delivery.
-
-One item leaves this effort without a home and is recorded here so it is not lost: the reverse
-proxy does not set `caServer`, so it defaults to the public authority's production endpoint.
-That is a fact about what runs today, not about the lab.
diff --git a/01-RESEARCH/004-lab-network/analysis.md b/01-RESEARCH/004-lab-network/analysis.md
deleted file mode 100644
index 5fc30277..00000000
--- a/01-RESEARCH/004-lab-network/analysis.md
+++ /dev/null
@@ -1,203 +0,0 @@
----
-effort: 004-lab-network
-updated: 2026-08-22
----
-
-# Reproducing the mesh network in a lab
-
-Established 2026-08-22 by reading the generating code and the live mesh DB. Every claim is a
-file location or a queried row.
-
----
-
-## 1. The network is data, not configuration
-
-`install.d/mesh-init.sh` and `install.d/adopt.sh` perform **no network configuration at
-all** — no WireGuard, no DNS, no firewall, no `/etc/hosts`. Every part of the network layer is
-generated by module hooks from mesh-DB rows:
-
-| layer | generated by | from |
-|---|---|---|
-| WireGuard interface + peers | `modules/wireguard/hooks/index.ts` (`postConfigure`) | `node_wg_keys`, `nodes.site`, `nodes.underlay_addr`, `module_env.WG_ADDRESS` |
-| `.internal` name resolution | `modules/dnsmasq-app/hooks/index.ts` | mesh config peers → `internal_domain` + `wg_address` |
-| public routing / vhosts | `modules/hal/sdk/src/vhost-gen.ts`, `feature-handlers/vhost.ts` | `vhosts:` manifests + `node_accessors` |
-| internal TLS | `modules/mesh-ca/hooks/index.ts` | a singleton CA row in the mesh DB |
-
-**Consequence:** a faithful lab is mostly a matter of writing the right rows. The network that
-results is produced by the same code production runs, which is the difference between testing
-the network and testing a model of it.
-
----
-
-## 2. The constraint that decides whether the lab works
-
-`modules/wireguard/hooks/index.ts:225-240` decides, per pair, whether to write an `Endpoint`:
-
-```js
-const isPrivate = (a) => /^(10\.|127\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.)/.test(a);
-
-if (coLocated && underlay) Endpoint = `${underlay}:${port}` // same LAN
-else if (underlay && !isPrivate(underlay)) Endpoint = `${underlay}:${port}` // public
-// else: no Endpoint — the peer must initiate, and we learn its endpoint from the handshake
-```
-
-A simulated public segment addressed out of RFC1918 space — `10.200.0.0/24`, say — makes the
-hub's underlay test as **private**. No spoke writes an `Endpoint` for the hub. Nothing can
-initiate. **No handshake ever occurs and the mesh silently never forms**, presenting as a
-WireGuard fault rather than an addressing choice.
-
-**The simulated public segment must therefore be `203.0.113.0/24`** — TEST-NET-3, reserved by
-RFC 5737 for documentation, guaranteed never to route on the real internet, and not matched by
-that regex. The code then treats it exactly as it treats a real hosting provider address.
-
-This is the single most important fact in this document.
-
----
-
-## 3. The topology being reproduced
-
-The shape below is what a mesh of this kind looks like: one node with a routable address, one
-publicly named but behind a household NAT, one stationary workstation, one that roams.
-Addresses use the documentation ranges of RFC 5737 and RFC 1918 throughout.
-
-| node | profile | site | underlay | WG | accessors |
-|---|---|---|---|---|---|
-| `anchor` | server | `dc` | `203.0.113.10` (routable) | `10.10.0.1/24` | `anchor.example` (public, primary) + `anchor.internal` (lan) |
-| `home-server` | server | `home` | `192.168.1.135` | `10.10.0.2/24` | `home-server.example` (public, primary) + `home-server.internal` (lan) |
-| `workstation` | workstation | `home` | `192.168.1.250` | `10.10.0.3/24` | `workstation.internal` (lan, primary) |
-| `laptop` | workstation | `NULL` | `NULL` | `10.10.0.4/24` | `laptop.internal` (lan, primary) |
-
-**Hub election is by convention, not by flag.** The hub is the node whose `profile='server'`
-*and* whose `WG_ADDRESS` begins `10.10.0.1` (`hooks/index.ts:188`). A lab must assign
-`10.10.0.1` to the node it intends as hub or there will be no hub.
-
-**`site` drives direct peering** (`hooks/index.ts:197-203`). Two nodes with the same non-null
-`site` peer directly with a `/32`; everything else routes through the hub's `/24`. A `NULL`
-site means roaming and hub-only — deliberately, because WireGuard has no failover and a more
-specific `/32` route to a dead endpoint blackholes rather than falling back.
-
-The four interesting pairs, all of which the lab must reproduce:
-
-| pair | behaviour | branch taken |
-|---|---|---|
-| anything → `anchor` | `Endpoint` written | underlay non-private |
-| `home-server` ↔ `workstation` | direct peer, LAN endpoints, keepalive | co-located |
-| **`anchor` → `home-server`** | **no `Endpoint`; learned from handshake** | not co-located, underlay private |
-| `laptop` → anything | hub only, always initiates | `site` is `NULL` |
-
-The third is the one worth building the lab for. The code comments at `hooks/index.ts:206-224`
-record what it cost to get right: testing `profile === "server"` was tried and was wrong,
-because a home-hosted node **is** a server yet is not publicly reachable — *"role does not
-imply reachability; the address does."* An earlier version aimed the hub at that node's public
-name, which hairpinned off the household NAT: 1.77 MiB sent, 0 B received, no handshake.
-
----
-
-## 4. The lab
-
-```
- br-wan 203.0.113.0/24 TEST-NET-3 — non-private, so the code treats it as public
- │
- ├── hub 203.0.113.10 profile=server site=dc WG 10.10.0.1
- │
- └── router VM 203.0.113.1 / 192.168.1.1
- │ NAT, plus one forwarded port to reproduce a published-but-NATed node
- │
- br-lan 192.168.1.0/24 identical to production, same host addresses
- ├── a 192.168.1.135 profile=server site=home WG 10.10.0.2
- └── b 192.168.1.250 profile=workstation site=home WG 10.10.0.3
-
- c — attach to br-lan, or br-wan ("away"), or detach ("asleep")
- underlay NULL profile=workstation site=NULL WG 10.10.0.4
-```
-
-Kept **byte-identical** to production: the LAN subnet and its host addresses, and the entire
-WireGuard plan. Only the public segment is substituted, and only because it must be.
-
-**The router earns its own VM.** It is what makes the published-but-NATed case real: that node
-is reachable from outside only through a forwarded port, and the hub must learn its endpoint.
-It also gives somewhere to break things — drop the forward and observe whether the mesh
-notices or whether the public name simply stops working.
-
-**Names.** A resolver on the wan side is authoritative for the public zone. `.internal` names
-need nothing extra: `dnsmasq-app` generates them from mesh config on each node, and writes an
-`/etc/hosts` block as a floor underneath, because a node must reach the mesh DB before its own
-DNS exists.
-
----
-
-## 5. What a node needs before any of this works
-
-Rows in the mesh DB — `nodes` (`name`, `user_name`, `profile`, `site`, `underlay_addr`),
-`node_accessors`, `module_env.WG_ADDRESS` (the hook hard-fails without it,
-`hooks/index.ts:114-116`), and `node_modules` assigning at least `wireguard`, `dnsmasq-app`,
-`mesh-ca`, and `traefik` where it serves.
-
-On disk beforehand, because the node must reach the mesh DB before it can read any of the
-above: registry database and object-store host and credentials, plus an npm token. This
-ordering — contact the mesh before the mesh has configured you — is itself worth reproducing,
-and is why the `/etc/hosts` floor exists.
-
-Everything else is generated: the WireGuard keypair locally (the private key never leaves the
-node; the public key is published to `node_wg_keys`), the peer list, the DNS records, the TLS
-leaf.
-
----
-
-## 6. Certificates — the lab issues its own
-
-**Settled 2026-08-22: the lab runs its own ACME issuer.**
-
-Public certificates use ACME **HTTP-01** via the reverse proxy, which requires genuine public
-reachability, so an isolated lab cannot use the real issuer. Rather than forgo certificate
-testing, the lab stands up an ACME server on its wan segment.
-
-**The lab keeps production's two-CA split rather than collapsing it.** Production issues
-public names from a public authority and internal names from the mesh CA; a lab with one CA
-would hide any bug living in that split. So:
-
-| | production | lab |
-|---|---|---|
-| public names | a public ACME authority | a test ACME server on the wan segment |
-| `.internal` names | `mesh-ca` | `mesh-ca`, unchanged |
-
-**A test issuer is the right shape, not a shortcut.** Purpose-built ACME test servers
-deliberately vary their behaviour — validation timing, nonce handling, chain composition — to
-expose assumptions a well-behaved authority would let pass. A lab CA that is *too* polite
-tests less than the real thing, not more.
-
-### It also exercises the port forward
-
-HTTP-01 means the issuer must reach the node being certified on port 80. In the lab:
-
-- the hub is directly reachable on the wan segment — straightforward
-- **the published-but-NATed node is reachable only through the router's forwarded port**
-
-So certificate issuance for that node passes only if the forward is correct. That is exactly
-why its certificate works in production, and it makes "the forward is missing" a reproducible
-failure rather than a mystery.
-
-### Required change: `caServer` must be configurable
-
-`modules/traefik/docker-compose.yml:17-19` sets the challenge entrypoint, the contact address
-and the storage path — but **no `caServer`**, so Traefik defaults to the public authority's
-*production* endpoint. Pointing the lab at its own issuer requires adding a `caServer` flag
-fed by an environment value, defaulting to production so real nodes are unaffected and the lab
-overrides it per node.
-
-Worth noting independently of the lab: aiming at the production endpoint rather than a staging
-one means every certificate experiment on a real node consumes production issuance quota, and
-a retry loop can exhaust it for a week. The lab issuer removes that exposure.
-
----
-
-## 7. Incidental finding: `scope:` is read by nothing
-
-Several manifests declare `scope: public` on firewall rules — `wireguard`, `traefik`, `gitea`,
-`mailu`, `qbittorrent`. It is **not part of the rule type** (`module-registry.ts:17-29`) and is
-**referenced by no code** in the firewall path. Real scoping is done with `from:`, as
-`modules/unifi/module.yml:52-93` does deliberately.
-
-So a manifest can appear to restrict a port to the public scope and in fact restrict nothing.
-This is the same shape as the rule in `00-META/how-we-build.md` — *an unenforced rule is
-indistinguishable from a wrong one, and costs more, because people believe it.*
diff --git a/01-RESEARCH/005-domain-grouping/00-overview.md b/01-RESEARCH/005-domain-grouping/00-overview.md
deleted file mode 100644
index 462badfd..00000000
--- a/01-RESEARCH/005-domain-grouping/00-overview.md
+++ /dev/null
@@ -1,87 +0,0 @@
----
-status: graduated
-initiated: 2026-08-23
-touches:
- - 02-DECISIONS/0009-modules-and-the-graph.md
- - 03-DESIGN/00-as-is/10-module-catalogue.md
- - 03-DESIGN/00-as-is/02-modules-and-manifests.md
-became:
- - 02-DECISIONS/0009-modules-and-the-graph.md
- - 02-DECISIONS/0009-modules-and-the-graph.md
----
-
-# 005 — Which domains the catalogue groups into
-
-[ADR 0009](../../02-DECISIONS/0009-modules-and-the-graph.md) settles
-that modules outside the platform core are grouped by domain rather than by single function,
-and deliberately does not settle the list. This effort settles the list — and, first, tests
-whether the premise survives measurement.
-
-## What is being investigated
-
-Eighty-nine modules sit outside the platform core. The question is which of them belong
-together, and the method is evidence rather than intuition: **which modules actually change
-together**, measured across the full history of the code repository.
-
-## Why
-
-The argument in ADR 0022 is that the catalogue's shape records what was installed rather than
-what anything is for — that four modules constituting "how a node is reachable" have no
-relationship the mesh can see, so a change to connectivity is made four times.
-
-That argument is testable. If those modules genuinely change together, the grouping is
-justified by more than tidiness. If they do not, the premise needs revising before a list is
-drawn from it.
-
-## What it touches
-
-The catalogue's shape, the manifest, and — for one candidate grouping — the provisioning
-reference itself, since a requirement names a provider **module**. Grouping providers would
-change what a consumer names.
-
-## Status
-
-Measurement is done and is in [`analysis.md`](analysis.md). It **partly contradicts the
-premise**, in a way that narrows the effort usefully:
-
-- Non-platform modules overwhelmingly change **alone** — 10% of commits touch more than one,
- and 50 of 89 never co-change with anything.
-- Two clusters do exist. One of them, reachability, holds up as a domain.
-- The other, the provisioned infrastructure providers, co-changes for a reason that argues
- **against** grouping rather than for it.
-
-The remaining work is the list itself, for the modules where grouping is justified, plus the
-open questions below.
-
-## What it became
-
-*Closed 2026-08-28.* Three of the four questions are answered, and by records that did not cite
-this effort — which is why it stayed open after being resolved.
-
-**Whether provider modules group at all** — *no.*
-[ADR 0009](../../02-DECISIONS/0009-modules-and-the-graph.md):
-there is no `networking` thing to install, there are concrete modules named individually. Folders
-assert relationships; edges record them. *Provider* stops being a category at the same time.
-
-**Whether "group or leave" is even the right pair of options** — *it was not*, and that is the
-useful finding. [ADR 0009](../../02-DECISIONS/0009-modules-and-the-graph.md)
-reframes it: things that change together share an **authority**, not a package. This effort's own
-measurement is what that record rests on — reachability being the *only* place modules genuinely
-co-change is why connectivity is a context and why nothing else needed one.
-
-**What to do with the ~50 modules that co-change with nothing** — *nothing.* They are modules.
-Grouping is a tag and a query over the graph, neither of which anybody keeps true by hand.
-
-**What remains is a sequencing question, not a grouping one**, and it moves rather than closes:
-*whether applications leave the monorepo before or after they group* is
-[research 009](../009-migration/00-overview.md)'s, because it is about how to get from here to
-there rather than about what the shape is.
-
-## Open (superseded by the above) questions
-
-| Question | Why it is open |
-|---|---|
-| Whether provider modules group at all, and if so what a consumer's requirement names instead of a module. | The provisioning reference is load-bearing; getting it wrong is expensive. Opinion and evidence in the analysis; not yet decided. |
-| Whether applications group into domains now and leave the monorepo later as a unit, or leave first. | Decided in principle — group first, then split — but the migration order has real cost either way. |
-| What to do with the ~50 modules that co-change with nothing. | The evidence gives no grouping signal for them at all. That may mean they are correctly sized already. |
-| Whether "group or leave" is even the right pair of options. | [Research 006](../006-mesh-from-scratch/code-skeleton.md) finds a third fate — **absorbed into the node host**, ceasing to be a module at all — and argues it is the correct answer for the reachability cluster this effort measured. If so, the cluster this effort found is evidence for absorption rather than for grouping. |
diff --git a/01-RESEARCH/005-domain-grouping/analysis.md b/01-RESEARCH/005-domain-grouping/analysis.md
deleted file mode 100644
index 6070c51c..00000000
--- a/01-RESEARCH/005-domain-grouping/analysis.md
+++ /dev/null
@@ -1,144 +0,0 @@
----
-effort: 005-domain-grouping
-updated: 2026-08-23
----
-
-# Which modules actually change together
-
-## Method
-
-Every commit in the code repository's main branch that touches the module catalogue, reduced
-to the set of modules it touched. Platform-namespace modules are excluded — their
-decomposition is settled by
-[ADR 0001](../../02-DECISIONS/0001-mesh-brokers-nodes-host-agents-think.md). Modules that no
-longer exist are excluded, because pre-rename names dominate the raw signal and describe a
-catalogue nobody works in.
-
-Commits touching more than eight modules are excluded from pair counting: a sweep across
-forty modules says the sweep happened, not that the modules are related.
-
-## Finding 1 — these modules overwhelmingly change alone
-
-| Measure | Value |
-|---|---|
-| Non-platform modules today | 89 |
-| Commits touching at least one | 312 |
-| Commits touching two to eight together | 33 — **10%** |
-| Modules that never co-change with anything | **50 of 89** |
-
-Nine commits in ten touch exactly one module. More than half the catalogue has never been
-edited alongside anything else.
-
-This is the first thing a grouping proposal has to survive, and it is not what the premise
-predicts.
-
-## Finding 2 — two clusters exist
-
-Everything above two co-changes falls into one of two groups.
-
-**The provisioned infrastructure providers.** The densest cluster: the relational stores with
-each other and with the cache, the broker, the object store and the image registry — a
-strongly connected set, six to two co-changes per pair.
-
-**Reachability.** The reverse proxy with the resolver, the firewall and the VPN; the firewall
-with the intrusion filter. Three to two co-changes per pair.
-
-Outside those: a torrent client with the VPN, twice. A login manager with a media player,
-twice. Nothing else reaches two.
-
-## Finding 3 — the provider cluster is manifest churn, not cohesion
-
-This is the result that matters, and it is only visible by reading the commits rather than
-counting them.
-
-Every multi-provider commit is a **cross-cutting change to the machinery, applied N times**:
-
-| Date | What the commit did |
-|---|---|
-| 2026-04-15 | modules own their tools — decentralised tool serving |
-| 2026-04-24 | filesystem-driven feature detection; manifest boilerplate removed |
-| 2026-04-27 | remove runtime dependencies that were only build dependencies |
-| 2026-05-01 | hooks move into their own directory with their own package |
-| 2026-05-05 | hooks renamed to a stage-and-feature convention; legacy install scripts removed |
-| 2026-08-06 | verify **by shape in the SDK**, not by copying a script into every module |
-| 2026-08-11 | bind declared volume paths that leaked an anonymous volume per deploy |
-| 2026-08-23 | scope services to the local network and mesh; respect the profile gate |
-
-**Not one of them is a change to what a database is.** They are all "the manifest contract
-changed, therefore every manifest changed".
-
-Two consequences follow.
-
-Merging the providers into one module would not have prevented a single one of these commits.
-It would have made the same edit land in one file rather than six — which is a diff-size
-improvement, not a boundary.
-
-And the history already shows the correct fix being applied, repeatedly and successfully:
-*verify by shape in the SDK, not by copying a script into every module*. When the same edit
-must be made in every provider, the answer that worked was **moving the concern into the
-machinery**, not merging the modules that carry it.
-
-Co-change here measures coupling to the manifest contract. It does not measure domain
-cohesion, and using it as though it did would group the catalogue by which modules are most
-boilerplate-heavy.
-
-## Finding 4 — reachability holds up
-
-The same reading applied to the reachability cluster gives a different answer. Most of its
-twenty-five multi-module commits are the same machinery churn — but not all, and the
-remainder are genuine:
-
-| Date | What the commit did |
-|---|---|
-| 2026-07-29 | let the mesh's proxy coexist with another listener on the same port |
-| 2026-08-03 | derive public split-DNS from node accessors so requests stop hairpinning |
-| 2026-08-06 | firewall mesh-only by default, public by declaration |
-
-Each is one intent — *change how a node is reachable* — landing across the proxy, the
-resolver, the firewall and the VPN together. That is exactly the shape ADR 0022 describes, and
-it is the only place in the catalogue where the measurement finds it.
-
-The 2026-08-23 scoping commit is the sharpest case: it spans the reachability cluster **and**
-two providers, because "which network is this exposed on" is a reachability question asked of
-a database.
-
-## What this means for ADR 0022
-
-The record's principle stands, and its scope needs narrowing. Grouping by domain is:
-
-- **Justified by evidence** for reachability. One intent, several modules, repeatedly.
-- **Argued against by evidence** for the providers. The coupling is to the manifest contract,
- and the demonstrated fix is to move the concern into the machinery.
-- **Unsupported either way** for the fifty modules that co-change with nothing. Silence is not
- evidence of independence — many are simply rarely touched — but there is no measured basis
- for grouping them, and a proposal that groups them is drawn from intuition. It should say so.
-
-The catalogue's shape is still wrong in the way the record describes. Measurement says
-grouping is the right fix in fewer places than the record implies, and that a second fix —
-moving cross-cutting concerns into the machinery — accounts for most of what looks like
-grouping pressure.
-
-## Recommendation on the provisioning reference
-
-Asked directly, and stated as an opinion because it is not yet decided.
-
-**Do not group the provider modules.** Three reasons, in order of weight:
-
-1. **The evidence for grouping them is the wrong evidence.** Finding 3.
-2. **A requirement names a provider module.** Grouping providers means a consumer names a
- resource type and something else chooses the implementation. That is not a folder move; it
- is implementation selection, a substantially larger design with its own failure modes, and
- nothing currently asks for it.
-3. **It would hide which implementation serves a requirement** — the one place the mesh most
- needs to be explicit, and precisely the indirection ADR 0022 warns grouping causes.
-
-A provider module is already exactly one purpose: it provisions one resource type. That is a
-boundary, not an accident of installation.
-
-## Still to do
-
-- Draw the list for the cases where grouping is justified, and say plainly which entries rest
- on measurement and which on judgement.
-- Decide the ~50 silent modules: correctly sized, or unmeasured?
-- Test the reachability grouping against the migration cost — the modules in it are among the
- most-changed in the catalogue, so churn during a move is not hypothetical.
diff --git a/01-RESEARCH/006-mesh-from-scratch/00-overview.md b/01-RESEARCH/006-mesh-from-scratch/00-overview.md
deleted file mode 100644
index 704ca940..00000000
--- a/01-RESEARCH/006-mesh-from-scratch/00-overview.md
+++ /dev/null
@@ -1,121 +0,0 @@
----
-status: graduated
-initiated: 2026-08-23
-touches:
- - 02-DECISIONS/0001-mesh-brokers-nodes-host-agents-think.md
- - 02-DECISIONS/0009-modules-and-the-graph.md
- - 03-DESIGN/00-as-is/00-overview.md
- - 03-DESIGN/01-to-be/00-work-breakdown.md
-became:
- - 02-DECISIONS/0004-a-node-and-how-it-joins.md
- - 02-DECISIONS/0005-the-node-host.md
- - 02-DECISIONS/0006-the-substrate-and-the-control-plane.md
- - 02-DECISIONS/0028-the-substrate-supplies-the-control-plane-and-nothing-else.md
- - 02-DECISIONS/0033-the-substrate-is-a-store-and-a-broker.md
- - 02-DECISIONS/0037-where-a-module-lives.md
- - 02-DECISIONS/0040-what-a-module-is.md
- - 02-DECISIONS/0041-events-are-a-relationship.md
- - 02-DECISIONS/0106-the-bus-is-nats.md
- - 03-DESIGN/01-to-be/05-the-node-host.md
- - 03-DESIGN/01-to-be/06-the-controller.md
- - 03-DESIGN/01-to-be/07-the-foundation.md
- - 03-DESIGN/01-to-be/08-connectivity.md
----
-
-# 006 — The mesh designed from nothing
-
-## What is being investigated
-
-What the mesh would look like if it were laid out today, with the requirements known and none
-of the accumulated shape — expressed as a **skeleton**: repositories at the root, modules
-inside them, and whatever turns out to be the right leaf unit below that.
-
-The deliverables are [`skeleton.md`](skeleton.md) — tiers, repositories and the four design
-moves — and [`code-skeleton.md`](code-skeleton.md) — the tier test, what a module looks like on
-disk, and where today's catalogue lands.
-
-## Why
-
-Every structural decision so far has been a **correction**: eight contexts replacing thirty-three
-modules ([ADR 0001](../../02-DECISIONS/0001-mesh-brokers-nodes-host-agents-think.md)), domains
-replacing single-function modules
-([ADR 0009](../../02-DECISIONS/0009-modules-and-the-graph.md)). A
-correction inherits the frame of the thing it corrects, and two of the mesh's oldest problems
-look unsolvable from inside that frame:
-
-- **The bootstrap circularity.** The mesh needs a database, a bus, a registry and an identity
- provider. Those are modules the mesh installs. The mesh cannot install them before it exists.
- This has been worked around repeatedly and never designed away.
-- **Participation requires privilege.** Everything assumes root on a machine whose packages and
- services the mesh owns. A phone cannot participate on those terms, and neither can a machine
- someone else administers.
-
-Designing from nothing is a way to find out which parts of the current shape are requirements
-and which are residue.
-
-**And there is more residue than expected, from a knowable source.** This began as a
-**dotfiles repository** — the first two days of history adopt dotfiles, add per-node dotfile
-overrides, and introduce service symlinking with an ignore file. The flat one-directory-per-tool
-catalogue, linking rather than copying, adoption of already-configured machines, per-node
-overrides, and the desktop modules are all inherited from that, not chosen for a mesh. Recorded
-in [`03-DESIGN/00-as-is/10-module-catalogue.md`](../../03-DESIGN/00-as-is/10-module-catalogue.md).
-
-That makes this effort's question sharper than "what would we do differently": much of what
-looks like design is a generalisation of *place files on my machines*, never revisited because
-it was never stated as an assumption.
-
-## The requirements this is designed against
-
-Stated by the operator, recorded here so the skeleton can be checked against them rather than
-against taste:
-
-1. The mesh manages multiple computers — **full control**, through modules installed to nodes.
-2. Mesh state lives in a **database**: which modules on which nodes, logs, configuration.
-3. Configuration has **several touchpoints** — tool surface, web interface, others — all hosted
- by the mesh itself.
-4. **Connectivity** is core: every node reachable from every other over a shared overlay, some
- nodes publicly exposed, firewalls configured.
-5. The mesh **hosts applications** — and requires some of them itself. This is the circularity.
-6. **Arch Linux only for now**; ideally any device, including phones, on lighter terms.
-7. The end goal is to **operate an IT company** on it — development, design, deployment, full
- circle, self-hosted. Personal cloud infrastructure.
-8. **Agents make it self-improving and self-healing.**
-9. It is **end-to-end testable on one machine**
- ([ADR 0016](../../02-DECISIONS/0016-the-lab.md)).
-
-## Status
-
-A first skeleton exists, with four design moves that the current shape does not have. It was
-held open because two of them were unproven and one contradicted a record that was already
-accepted; how each was settled is under *What it became* below.
-
-**Finding worth stating up front:** [ADR 0001](../../02-DECISIONS/0001-mesh-brokers-nodes-host-agents-think.md)
-names nine bounded contexts and **none of them owns connectivity** — no overlay, no resolution,
-no firewall, no ingress. Requirement 4 has no home in the accepted decomposition, while
-[research 005](../005-domain-grouping/analysis.md) found reachability to be the *only* part of
-the catalogue where modules genuinely change together under one intent. The skeleton adds it.
-
-## Open questions
-
-| Question | Why it is open |
-|---|---|
-| Does the record — the event log contexts integrate through — belong to the substrate or the control plane? | It is infrastructure by shape and domain by content. Placing it wrong reintroduces a circularity. |
-| One repository per tier, or per context? | Already open from ADR 0001 as "catalogue destination — one repository or many". The skeleton assumes per tier and does not settle it. |
-| ~~Does an unprivileged node earn a place in the inventory, or only a presence?~~ | **Answered 2026-08-25** by the operator: a node is a *managed machine inside the mesh*, not an unprivileged something — and a disconnected node is still a node, in a different situation. The question posed a class distinction; the answer is that there is none, and what varies is **state**. Recorded as [ADR 0004](../../02-DECISIONS/0004-a-node-and-how-it-joins.md). |
-| ~~Does absorbing overlay, filtering, packages, supervision and the container runtime make the host too large?~~ | **Answered 2026-08-25** — [`host-size.md`](host-size.md). Measured: the absorption is smaller than the machinery that already applies state, and eight of ten adapters already carry no dependency. The risk is not size but direction, and it is two modules wide. The claim survives with its scope corrected — the host carries one concern, *apply declared state on this machine*, of which the six are instances. Recorded as [ADR 0005](../../02-DECISIONS/0005-the-node-host.md), designed in [`05-the-node-host.md`](../../03-DESIGN/01-to-be/05-the-node-host.md). |
-| ~~Four substrate services or five?~~ | **Answered conditionally**, which is the honest form — [`07-the-foundation.md`](../../03-DESIGN/01-to-be/07-the-foundation.md). The substrate is *what the control plane consumes and cannot grant itself*. The identity provider qualifies only if the control plane delegates authentication; if it authenticates natively it is an ordinary hosted service. The count follows from a decision not yet taken, and asserting four was asserting that decision. |
-| Does `feature` survive? | The skeleton splits it in two and argues the conflation is what makes the delivery pipeline hard to reason about. Unproven. |
-
-## What it became
-
-*Closed 2026-10-10.* The skeleton's moves became decisions and designs, and its open questions were settled by them.
-
-- **Where the record lives** is not decided. Events became a declared relationship ([ADR 0041](../../02-DECISIONS/0041-events-are-a-relationship.md)) carried by
- the bus ([ADR 0106](../../02-DECISIONS/0106-the-bus-is-nats.md)), and the foundation is exactly a store and a bus ([ADR 0033](../../02-DECISIONS/0033-the-substrate-is-a-store-and-a-broker.md), after
- [ADR 0006](../../02-DECISIONS/0006-the-substrate-and-the-control-plane.md) and [ADR 0028](../../02-DECISIONS/0028-the-substrate-supplies-the-control-plane-and-nothing-else.md)), but no record says what the record is or where it lives. It is tracked
- by issue 414.
-- **One repository per tier or per context**: [ADR 0037](../../02-DECISIONS/0037-where-a-module-lives.md).
-- **Does `feature` survive**: no. [ADR 0040](../../02-DECISIONS/0040-what-a-module-is.md) defines a module by the software it delivers, not as a bucket of
- features; *artifact* became the word for what a build produces.
-- The answered rows above became [ADR 0004](../../02-DECISIONS/0004-a-node-and-how-it-joins.md) and [ADR 0005](../../02-DECISIONS/0005-the-node-host.md), and the designs [to-be 05](../../03-DESIGN/01-to-be/05-the-node-host.md), [to-be 07](../../03-DESIGN/01-to-be/07-the-foundation.md) and
- [to-be 08](../../03-DESIGN/01-to-be/08-connectivity.md).
diff --git a/01-RESEARCH/006-mesh-from-scratch/code-skeleton.md b/01-RESEARCH/006-mesh-from-scratch/code-skeleton.md
deleted file mode 100644
index 62b1c420..00000000
--- a/01-RESEARCH/006-mesh-from-scratch/code-skeleton.md
+++ /dev/null
@@ -1,272 +0,0 @@
----
-effort: 006-mesh-from-scratch
-updated: 2026-08-23
----
-
-# The code skeleton
-
-[`skeleton.md`](skeleton.md) laid out tiers and repositories. This is the level below: what a
-module looks like on disk, and — the question that forced a correction — where a given piece of
-today's catalogue actually lands.
-
-## The tier test
-
-A decision procedure, so placement is answerable rather than argued. Ask in order; first match
-wins:
-
-1. **Does it apply state on a machine?** → tier 0, inside the host.
-2. **Can the control plane *start* without it?** If *no* → tier 1, substrate. Note the verb:
- *start*, not *function fully*. A capability the control plane loses without something is not
- the same as a thing it cannot come up without — see "becoming self-hosting" below.
-3. **Does it decide what should be true across nodes?** → tier 2, a control-plane context.
-4. **Is it a way to talk to tier 2, holding no logic of its own?** → tier 3, a surface.
-5. **Otherwise** → tier 4, a workload.
-
-## Worked example — where postgres ends up
-
-The obvious answer is "twice": once as the mesh's own database in the substrate, once as a
-hosted database in the catalogue. That answer is wrong, and seeing why fixes something.
-
-Run the test. *Can the control plane exist without a relational store?* No. **Postgres is
-tier 1.** It lives once:
-
-```
-mesh-substrate/store/postgres/
-```
-
-There is no second copy in the catalogue, because the thing that differs between the mesh's own
-database and a project's database is **not the module**. It is how that instance is brought up:
-
-| | The mesh's own instance | A project's database |
-|---|---|---|
-| Brought up by | the host, from the pinned bundle, with no control plane present | the ordinary delivery and provisioning path |
-| Declared in | `mesh-substrate/bundle.yml` | the consuming module's `requires:` |
-| Exists because | the control plane cannot start without it | something asked for it |
-
-Same module, two roles. **Tier is a property of the module** — what must exist before what — and
-**the bundle is a property of the mesh's own instance.**
-
-This is also legal under the dependency rule, which is worth checking rather than assuming: a
-tier-4 workload that requires a database depends on tier 1, which points *downward*. The
-inverse — substrate reaching into the catalogue for a module — would not be, and is the shape
-the naive "twice" answer would have created.
-
-The same reasoning places the rest of the substrate: the bus, the object store, the image
-registry. Each fails step 2, each lives once, each is pinned. That is the whole substrate: four
-services, and the deliberate absence of a fifth.
-
-**The identity provider was raised as a boundary case and is settled: it is not substrate.**
-The mesh does not require one — tier 2 authenticates its own callers natively, and an identity
-provider is a service the mesh hosts like any other. The substrate is four services, not five.
-
-The test earned its keep here by turning a vague unease into one answerable question — *does the
-control plane delegate authentication?* — rather than a debate about how important identity
-feels.
-
-## Becoming self-hosting — the forge and the registries
-
-Self-improvement means the mesh hosts the things it improves itself with: a forge, an image
-registry, a package registry. The obvious worry is that these duplicate — a `mesh-gitea`
-for the mesh and a `gitea` for everyone else. **They do not, and the reason is worth stating
-carefully, because it is the same reason the bootstrap keeps failing today.**
-
-### They are not substrate
-
-Run the test with the sharpened verb. *Can the control plane start without a forge?* **Yes.** It
-comes up, holds inventory, answers questions and manages nodes with the modules it already has.
-What it cannot do is **change itself**. That is a capability, not a precondition.
-
-So: forge, image registry and package registry are **tier 4 workloads**. One module each, in the
-catalogue, exactly like the media server. There is no mesh-specific copy.
-
-This is not a technicality. It buys a property worth having: **if the forge dies, the mesh keeps
-running.** Nodes stay managed, services stay up, only self-modification stops. Putting the forge
-in the substrate would make losing it fatal, for no gain.
-
-### But delivery needs them — is that not an upward dependency?
-
-It would be, stated naively, and that would break the one rule the whole skeleton rests on.
-
-It is resolved the way the constitution already says to resolve it — **depend on abstractions,
-not on concrete dependencies**. Tier 2's delivery context does not require *the forge module*.
-It declares requirements:
-
-| Delivery requires | Satisfied by |
-|---|---|
-| a source of record for module code | whichever module provides it |
-| somewhere to publish images | whichever module provides it |
-| somewhere to publish packages | whichever module provides it |
-| somewhere to put build artifacts | the substrate's object store |
-
-Tier 2 defines the requirement; tier 4 provides the implementation; the binding is data. The
-dependency points **downward from the provider to the interface**, which is legal, and the
-control plane never names a concrete module.
-
-The mechanism for this already exists and is the mesh's most valuable one: **provisioning**. A
-module declares what it provides; a consumer declares what it requires; the mesh binds them.
-The only new idea is that **the control plane is itself a consumer** — it has requirements, and
-they are satisfied the same way a workload's are.
-
-That generalisation is significant enough to need its own study, and is not settled here.
-
-### Self-hosting is a state the mesh reaches, not a precondition
-
-This is the part today's mesh gets wrong, and it explains a recurring class of pain.
-
-A first node comes up from **pinned external artifacts** — upstream images, by digest, carried
-in the bundle. It has to: the mesh's own registry does not exist yet, and cannot. The mesh at
-this point is running and manages nodes, and is not yet self-hosting.
-
-Self-hosting is then **reached**: the forge is installed as an ordinary workload, the mesh's own
-source moves into it, the registries come up, and delivery's requirements are re-bound from
-external providers to internal ones. From that point the mesh builds and deploys itself.
-
-Stated as a lifecycle:
-
-```
-pinned external artifacts ─► mesh runs, manages nodes
- │
- │ forge + registries installed as workloads
- │ delivery's requirements re-bound
- ▼
- mesh builds and deploys itself
-```
-
-**Today's mesh assumes the second state from the first moment.** Its source, its packages and
-its images are all expected to be self-hosted before there is anything to host them — which is
-why raising a first node needs a script that exists solely to paper over the impossibility, and
-why that script is the least-exercised path in the system.
-
-Making the transition explicit has a second benefit: it is reversible. A mesh whose forge is
-broken can re-bind delivery to external providers and keep improving itself while it repairs
-the forge. Today that escape hatch does not exist, because the dependency is not expressed
-anywhere it could be changed.
-
-### So, concretely
-
-One `gitea` module. One image-registry module. One package-registry module. Each a tier-4
-workload. The mesh's own instances are distinguished from any other instance **by what they are
-bound to, not by being different modules** — precisely the same answer as postgres, arrived at
-by the same test.
-
-## The five fates of a module
-
-Research 005 asked whether a catalogue module should be **grouped into a domain** or **leave
-the repository**. Working the tier test across the catalogue surfaces a third answer that
-neither option covers, and it is the most common one.
-
-| Fate | Means | Examples from today |
-|---|---|---|
-| **Absorbed into the host** | It is not a module at all. It is part of what "managing a machine" means, and belongs in tier 0. | overlay membership, packet filtering, package management, service supervision, container runtime, filesystem management |
-| **Substrate** | The control plane cannot exist without it. Pinned, host-applied. | relational store, bus, object store, image registry |
-| **Control-plane context** | It decides something across nodes. | connectivity policy, inventory, delivery, provisioning, observability |
-| **Workload module** | The mesh hosts it. Grouped per [ADR 0009](../../02-DECISIONS/0009-modules-and-the-graph.md). | media library, desktop session, collaboration tooling |
-| **Leaves the repository** | A standalone application, per [ADR 0015](../../02-DECISIONS/0015-applications-live-in-their-own-repository.md). | the applications identified in research 005 |
-
-**The first fate is the finding.** Research 005 measured the reachability cluster — proxy,
-resolver, firewall, overlay — as the only place in the catalogue where modules genuinely change
-together under one intent. The skeleton explains *why*: they are not four modules that ought to
-be one domain module. They are four facets of one thing the host should own, currently
-expressed as modules because a module was the only unit available.
-
-Under this skeleton the overlay module and the firewall module **stop existing**. The host holds
-membership and applies filtering; tier 2 decides the policy; the swappable backends stay
-modules. That is a different and better answer than grouping them, and it was not visible from
-inside the current frame.
-
-It also partly answers research 005's other open question — the fifty modules that co-change
-with nothing. Several are host concerns rather than domains: package management, container
-runtime, filesystem tooling. Silence was the right signal after all; the wrong conclusion was
-that grouping was the only available fix.
-
-## What a module looks like on disk
-
-Per [`skeleton.md`](skeleton.md) Move 4, a module declares **parts** — independently selectable
-pieces of desired state — and produces **artifacts** — things built once per version.
-
-```
-/
- module.yml identity, what it provides, what it requires,
- which host profiles it can land on
- parts/
- service/ desired state: container, volumes, exposure
- provisioner/ how it grants its resource to consumers
- migrations/ its own persistent state
- tools/ capabilities it contributes
- artifacts/
- / source for something built and published
-```
-
-A module with no artifacts consumes an upstream image and builds nothing. A module with no
-parts is not a module.
-
-Worked through for the substrate's relational store:
-
-```
-mesh-substrate/store/postgres/
- module.yml provides: database · profiles: [managed]
- parts/
- service/ the container, its volume, its network exposure
- provisioner/ grants a database and role to a consumer
- migrations/ none — it holds no state of its own
- artifacts/ none — upstream image, pinned by digest in bundle.yml
-```
-
-## The tree, at file level
-
-```
-mesh-host/ TIER 0
- cmd/host/
- internal/
- apply/ reconcile declared state
- inventory/ what this machine is and can do
- link/ outbound connection to the control plane
- overlay/ membership: address, keys, tunnel
- filter/ packet filtering from tier-2 policy
- packages/ package management
- services/ supervision
- containers/ container runtime
- store/ embedded local state
- profile/ managed · user · edge
- substrate.lock pinned tier-1 descriptor
-
-mesh-substrate/ TIER 1
- bundle.yml the pinned set, by digest
- store/postgres/
- bus//
- objects//
- images//
-
-mesh-control/ TIER 2
- record/ the event log contexts integrate through
- inventory/ nodes · modules · assignments · versions
- config/ settings · secrets · derivation
- connectivity/ addresses · resolution · exposure · filtering · certificates
- provisioning/ grants between modules
- delivery/ source → artifact → node
- observability/ health · logs · metrics
- identity/ agents · humans · services · authorisation
- work/ tasks · workflows · runs
- knowledge/ memory · documents · retrieval
- api/ the one interface surfaces speak to
-
-mesh-surfaces/ TIER 3
- tools/ web/ cli/
-
-mesh-catalog/ TIER 4
- // layout as above
-
-mesh-lab/ mesh-sdk/ hq/ (company-scoped — ADR 0019)
-```
-
-## What this does not settle
-
-- **The identity boundary case.** Four substrate services or five.
-- **Where the record lives.** Still the open question from
- [`00-overview.md`](00-overview.md), and the tier test does not resolve it: the record is
- needed by tier 2 and is *of* tier 2, which is exactly the shape that produces a circularity.
-- **Whether absorbing into the host makes the host too large.** Six internal concerns is already
- a lot for a binary whose whole argument is that it has no dependencies. The counter-argument
- is that each is small and none can be optional — but this is the skeleton's biggest unproven
- claim, and it should be tested by writing the host's interface before anything else.
-- **The migration.** Nothing here says how today becomes this.
diff --git a/01-RESEARCH/006-mesh-from-scratch/host-size.md b/01-RESEARCH/006-mesh-from-scratch/host-size.md
deleted file mode 100644
index e8383f2c..00000000
--- a/01-RESEARCH/006-mesh-from-scratch/host-size.md
+++ /dev/null
@@ -1,126 +0,0 @@
-# Is the host too large?
-
-The skeleton absorbs overlay membership, packet filtering, package management, service
-supervision, the container runtime and filesystem management into tier 0, and
-[`00-overview.md`](00-overview.md) calls this *"the skeleton's biggest unproven claim. A binary
-whose whole argument is that it has no dependencies now carries six concerns."*
-
-This is that claim, measured.
-
-## Method
-
-Against `origin/main` of the monorepo at 2026-08-25 — read through git refs rather than a
-checkout, which sits on a feature branch 1031 commits behind with uncommitted work.
-
-For each module that implements one of the six concerns: how much code it is, and **what it
-depends on to do its job**. The second question turned out to be the one that matters.
-
-## Finding 1 — by size, the concern is misplaced
-
-| Absorbed into the host | Lines |
-|---|---|
-| `dnsmasq-app` | 706 |
-| `traefik` | 426 |
-| `ufw` | 385 |
-| `mesh-ca` | 353 |
-| `wireguard` | 313 |
-| `incus` | 285 |
-| `package-manager` | 118 |
-| `zfs` | 67 |
-| `fail2ban` | 60 |
-| `docker-app` | 42 |
-| **total** | **2 755** |
-
-| Machinery that already applies state on a node | Lines |
-|---|---|
-| `hal/meshware` | 1 829 |
-| `hal/env-sync` | 626 |
-| `hal/config-sync` | 604 |
-| **total** | **3 059** |
-
-**Everything being absorbed is smaller than the machinery that already exists to apply it.**
-The six concerns are not six subsystems; they are ten thin adapters averaging 275 lines, most
-of which is rendering a config file and running a command.
-
-The host is not a new large thing. It already exists, spread across three core modules, and
-the absorption adds less code than those three already contain.
-
-Size is therefore the wrong axis, and the open question asked about the wrong thing.
-
-## Finding 2 — the real risk is direction, and it is narrow
-
-What each adapter needs in order to decide what to write:
-
-| Module | Gets its inputs from | Applier only? |
-|---|---|---|
-| `ufw` | `~/.hal/modules/*/module.yml` on the local disk | yes |
-| `dnsmasq-app` | `process.env.DNSMASQ_*`, derived centrally by `env-sync` | yes |
-| `fail2ban`, `mesh-ca`, `package-manager`, `docker-app`, `incus`, `zfs` | local state and declared env | yes |
-| `wireguard` | **direct `pg` connection** — `nodes`, `node_accessors`, `node_wg_keys`, `module_env` | **no** |
-| `traefik` | **direct `pg` connection** — `nodes`, `mesh_ca` | **no** |
-
-**Eight of ten are already pure appliers.** They receive derived state and put it on the
-machine. Absorbing those into tier 0 moves no dependency at all — it moves code that already
-has none.
-
-**Two reach upward.** `wireguard` and `traefik` open a connection to the control plane's
-database and compute their own configuration from it. Absorbing them *as they are* would put a
-Postgres client and knowledge of the mesh schema inside tier 0 — an upward dependency, which
-is precisely what
-[the tier rule](skeleton.md) forbids and what the entire bootstrap argument rests on.
-
-So the danger in the absorption is real, and it is two modules wide rather than six concerns
-wide.
-
-## Finding 3 — the split has already been happening, unnamed
-
-`dnsmasq-app` is the same *kind* of module as `wireguard`: it needs every node's addresses and
-names. It does not query for them. Its own comments record why:
-
-> *"the mesh DB already holds [this] in `node_accessors` and the WireGuard address, duplicated
-> by hand on all four nodes. Renaming the namespace then meant editing four override rows
-> nobody [knew about]."*
-
-and
-
-> *"now derived from `node_accessors`"*
-
-That is one module having already made the move the skeleton proposes — deciding centrally,
-applying locally — for the ordinary reason that hand-duplicated state went wrong. Nobody named
-it as an architectural direction; it was reached by fixing a bug.
-
-Eight of ten adapters are on the far side of that migration. Two are not.
-
-## What this means for the open question
-
-**The absorption is not a move. It is a split, and it is mostly already done.**
-
-The question "does the host become too large" assumed six concerns would arrive whole. They do
-not. Each divides:
-
-- **deciding** — what this node's overlay, names, exposure and filtering should be, which needs
- every other node and therefore belongs in tier 2;
-- **applying** — putting that on the machine, which needs root and locality and therefore
- belongs in tier 0.
-
-Tier 0 absorbs the applying. That is ~2 755 lines today, most of it already dependency-free,
-against 3 059 lines of apply machinery the host needs regardless.
-
-**The claim survives, with its scope corrected.** The host does not carry six concerns; it
-carries one — *apply declared state on this machine* — of which the six are instances. That is
-the skeleton's own tier-0 test (*"does it apply state on a machine?"*) applied to itself.
-
-## What remains open
-
-- **The two unsplit modules are the two hardest.** Overlay needs every node's key, address,
- site and endpoint reachability; the proxy needs certificates and every node's exposed names.
- These are where "derive centrally, apply locally" is most work, and neither has been done.
- The measurement says the design is right; it does not say the migration is cheap.
-- **Six concerns is still six vocabularies.** Absorbing them adds no dependencies but does add
- surface: the host must know what a WireGuard peer, an nftables rule, a package, a unit, a
- container and a dataset *are*. Nothing here measures that cost, and it is the residue of the
- original worry.
-- **What the host must carry versus what it must find.** The host manages `wg`, `nft`,
- `pacman`, `docker`; it does not contain them.
- [Issue 007](../../04-ISSUES/007-an-installed-package-is-not-a-capability/00-report.md) is
- exactly this question and is unresolved.
diff --git a/01-RESEARCH/006-mesh-from-scratch/skeleton.md b/01-RESEARCH/006-mesh-from-scratch/skeleton.md
deleted file mode 100644
index 502d5ece..00000000
--- a/01-RESEARCH/006-mesh-from-scratch/skeleton.md
+++ /dev/null
@@ -1,281 +0,0 @@
----
-effort: 006-mesh-from-scratch
-updated: 2026-08-23
----
-
-# The skeleton
-
-Repositories at the root, modules inside them, parts at the leaf. Four tiers, and a dependency
-rule that only points downward.
-
-## What a tier is
-
-A tier answers one question: **what has to exist before this can exist?** It is not importance,
-and it is not a layer in the networking sense. It is bootstrap order, made explicit.
-
-Walk a bare machine to a running mesh and the tiers fall out of the story:
-
-```
-bare machine
- │ one command lands ONE binary. Nothing else exists. TIER 0 host
- │
- │ it reads a pinned file it already carries and raises a
- │ database, a bus, an object store and an image
- │ registry — locally, alone. TIER 1 substrate
- │
- │ on those, the mesh's brain starts: which nodes exist,
- │ what runs where, what is reachable. TIER 2 control plane
- │
- │ ways to talk to that brain. TIER 3 surfaces
- │
- └ everything the mesh then carries. TIER 4 workloads
-```
-
-**Dependencies point only downward.** The substrate never references the control plane. That
-one constraint is the entire bootstrap answer, because it guarantees there is always a place to
-start.
-
-Why it earns its keep here: **the current mesh violates this, and that is the circularity that
-keeps recurring.** The mesh database is a module; modules are installed by the delivery
-pipeline; the pipeline needs the database. No order works, so a first-node script exists to
-paper over it, and every later substrate change has to pretend the problem is not there.
-
-A tier is not a repository and not a bounded context. Those are different cuts: a context says
-*who owns this concept*, a tier says *what must already be running*.
-
-## The tree
-
-```
-mesh-host/ TIER 0 — the only thing ever installed by hand
- apply/ reconcile declared state on this machine
- inventory/ what this node is, has, and is capable of
- link/ the single outbound connection to the control plane
- store/ embedded local state — authoritative while disconnected
- profile/ capability detection: managed · user · edge
- substrate.lock pinned tier-1 descriptor, appliable with no mesh present
-
-mesh-substrate/ TIER 1 — declarations only, no logic of its own
- store/ relational state
- bus/ commands and events
- objects/ blobs and build artifacts
- images/ container images
- bundle.yml the pinned set tier 0 can raise alone
-
-mesh-control/ TIER 2 — the control plane
- record/ the event log every context integrates through
- inventory/ nodes · modules · assignments · versions
- config/ settings · secrets · derivation onto nodes
- connectivity/ overlay · resolution · exposure · filtering · certificates
- provisioning/ resource grants between modules
- delivery/ source → artifact → node
- observability/ health · logs · metrics · alerts
- identity/ agents · humans · services · authorisation
- work/ tasks · workflows · runs
- knowledge/ memory · documents · retrieval
- api/ the one interface every surface speaks to
-
-mesh-surfaces/ TIER 3 — thin; no logic lives here
- tools/ the agent-facing tool surface
- web/ the operator-facing interface
- cli/ the shell-facing interface
-
-mesh-catalog/ TIER 4 — what the mesh hosts
- / grouped per ADR 0022, list per research 005
-
-mesh-lab/ the whole mesh, disposable, on one machine
-mesh-sdk/ contracts shared across tiers — types, not behaviour
-
-hq/ company-scoped, not a mesh repository — ADR 0019
-```
-
-## The dependency rule
-
-**A tier may depend only on tiers below it.** Substrate never references the control plane.
-The control plane never reaches into a node except through the host. A surface holds no logic
-a second surface would have to reimplement.
-
-This is the whole of the bootstrap answer, and per this repository's own rule it must say how
-it is checked: a dependency-direction lint in the build, failing on an upward import. A tier
-rule enforced by intention is the same as no tier rule — that is
-[ADR 0010](../../02-DECISIONS/0010-delivery.md) applied to architecture.
-
-## Move 1 — the substrate is applied, not delivered
-
-**The problem.** The mesh needs a database, a bus, an object store and an image registry.
-Today those are modules, and modules are installed by the delivery pipeline, which
-needs the database and the bus. The first node is therefore raised by a special script that
-exists only because of the circularity, and every later change to the substrate has to pretend
-the circularity is not there.
-
-**The move.** The host can apply a declaration without anyone telling it to. The substrate is
-a **pinned bundle** the host carries: a fixed, versioned, self-contained descriptor of the
-four services and nothing else. Raising a first node is `host apply substrate.lock` — not a
-special path, just the ordinary one with no control plane on the other end.
-
-The circularity disappears rather than being worked around: **the substrate is applied by tier
-0, the mesh is delivered by tier 2, and they are different mechanisms on purpose.**
-
-The price is real and should be named: the substrate is upgraded by bumping a pin and
-re-applying, not by the pipeline. It gets less machinery than everything else — no per-node
-selection, no provisioning, no fan-out — and that is the point. Five services justify a
-simpler mechanism than a hundred.
-
-## Move 2 — the host is one binary with capability profiles
-
-**The problem.** Everything assumes root on a machine whose packages, services and network the
-mesh owns. A phone cannot offer that, and neither can a work laptop. The current answer would
-be a lightweight fork, which means two implementations and one of them rotting.
-
-**The move.** One host binary, and a **profile** it detects rather than is told:
-
-| Profile | Can | Typical |
-|---|---|---|
-| `managed` | packages, services, network, filesystem — the full surface | a machine the mesh owns |
-| `user` | user-level services and tools; no package or network management | a shared or administered machine |
-| `edge` | report presence, relay, expose a tool surface; hold nothing | a phone |
-
-A module declares which profiles it can land on. Assignment to an incapable node fails at
-declaration time, not at deploy time — a phone is not a machine that fails to install a
-firewall; it is a node the firewall module cannot be assigned to.
-
-This makes the phone case a **capability question rather than a platform question**, which is
-what keeps it from becoming a second implementation. It also removes the current unstated
-assumption that every node is equivalent — already false, and today handled by remembering.
-
-## Move 3 — connectivity becomes a context
-
-[ADR 0001](../../02-DECISIONS/0001-mesh-brokers-nodes-host-agents-think.md) names nine contexts
-and none of them owns the overlay, the resolver, the firewall or the ingress. `config` owns
-PKI, which is the closest thing, and it is not close.
-
-Meanwhile [research 005](../005-domain-grouping/analysis.md) measured the whole catalogue and
-found that reachability is the **only** place where modules genuinely change together under one
-intent — the proxy with the resolver, the firewall with the overlay, repeatedly, because *how a
-node is reachable* is one question asked in four places.
-
-So the evidence and the gap point the same way. `connectivity` owns:
-
-- the overlay every node joins, and the addresses on it
-- name resolution, internal and public
-- exposure — which services answer from outside, on which names
-- filtering — what may reach a node at all
-- certificates for both name spaces
-
-This is an addition to an accepted record, so it is a decision, not a drafting choice. It
-belongs in a new record that extends ADR 0001 the way
-[ADR 0009](../../02-DECISIONS/0009-modules-and-the-graph.md) does —
-not written here.
-
-### Where the networking actually lives
-
-A context is an authority, not a running thing, so naming one does not say who brings the
-overlay up. That splits three ways, and the split is the design:
-
-| Concern | Tier | Why there |
-|---|---|---|
-| **Overlay membership** — this node joins, holds an address, keeps the tunnel up | **0, in the host** | Everything cross-node needs it *before* the substrate is reachable from elsewhere. A module cannot provide it, because installing a module is itself a cross-node operation. |
-| **Policy** — who holds which address, what resolves, what is exposed, what is filtered | **2, `connectivity`** | Bookkeeping and authority. It decides; it runs nothing. |
-| **Machinery** — resolver, reverse proxy, firewall backend, certificate issuance | **4, modules** | Swappable, and not every node needs them. A node without a reverse proxy is still a node. |
-
-There is a second circularity hiding here, and it has to be closed explicitly: **the host's link
-to the control plane does not run over the overlay.** If it did, the overlay would have to be up
-before the host could be told how to join it. The link is ordinary outbound internet to a public
-endpoint; the overlay carries node-to-node traffic only.
-
-Joining is therefore: host lands → links out with a join token → control plane returns an
-address and keys → host raises membership → the substrate on other nodes becomes reachable.
-
-This also makes the `edge` profile honest rather than special-cased. A phone can hold overlay
-membership in userspace without privilege, and cannot run the machinery. That is the profile
-distinction doing its job.
-
-## Move 4 — `feature` splits in two
-
-The invitation was to check whether the concept survives. It does not, in one piece.
-
-Today a **feature** means both *a thing built once* and *a thing selected per node*, and the
-delivery pipeline is hard to reason about precisely because those have different cardinality
-and one word ([ADR 0010](../../02-DECISIONS/0010-delivery.md)
-is the pipeline half of the same confusion).
-
-Split it:
-
-| Concept | Is | Cardinality |
-|---|---|---|
-| **artifact** | something built and published — an image, a bundle, a package | once per module version |
-| **part** | an independently selectable piece of a module's desired state | chosen per node, per assignment |
-
-A module declares desired state in parts, and produces artifacts. An assignment names the node
-and the parts. Delivery builds artifacts once and applies parts per node — and the two words
-now carry the two cardinalities that the pipeline already has.
-
-This keeps what features are genuinely for — per-node opt-in of *some* of a module, which the
-work breakdown already calls for — and drops the conflation that makes the current model
-confusing.
-
-## What the mesh is, versus what it hosts
-
-Tiers 0–3 are the mesh. Tier 4 is everything it carries, and the boundary is stated by
-requirement rather than by taste: **a module is part of the mesh if removing it stops the mesh
-managing nodes.** A media server does not. A relational store does — which is why it sits in the
-substrate and not the catalogue, despite being, in every other respect, an application like any
-other. An identity provider, notably, does **not**: the control plane authenticates its own
-callers, so identity is a hosted service like the media server.
-
-That test also settles the IT-company goal without a special category. Development, design and
-deployment tooling are **workloads** — tier 4, hosted, provisioned, delivered like anything
-else. The mesh does not grow a "company" feature; it hosts the tools a company runs on, and
-the fact that it runs its own development on them is dogfooding, not architecture.
-
-## What agents are, structurally
-
-Self-improvement and self-healing are not a tier. Agents are participants
-([ADR 0003](../../02-DECISIONS/0003-agents-are-persistent-employees.md)) that hold identity in
-tier 2, act through tier 3 like any other caller, and run as workloads in tier 4.
-
-This matters for one reason: **an agent must not have a privileged path**. Anything an agent
-can do to the mesh, a person can do through the same surface, and anything it cannot express
-through the tool surface is a gap in the surface rather than a reason for a back door. Self-
-healing built on a private channel is unreviewable, and would be the one part of the mesh with
-no human checkpoint.
-
-## How this is tested
-
-The lab ([ADR 0016](../../02-DECISIONS/0016-the-lab.md)) raises the
-tree above on one machine: virtual machines as nodes, a real overlay between them, the real
-substrate bundle, the real control plane, the real delivery path.
-
-The tier rule is what makes that affordable. A scenario needing only tiers 0 and 1 is one
-virtual machine and a pinned bundle — which is also, exactly, the bootstrap path. **The
-hardest thing to test becomes the cheapest scenario to run**, and the first-node path stops
-being the one thing nobody exercises until it breaks.
-
-## What this skeleton does not answer
-
-- Where the record lives. It is infrastructure by shape and domain by content, and putting it
- in the substrate risks recreating a circularity in the one place the design just removed one.
-- Whether tier 2's contexts are one repository or several. Open from ADR 0001 already.
-- Whether an `edge` node is in the inventory or merely present — which decides whether "node"
- is one concept or two.
-- The migration. Nothing here says how today's mesh becomes this, and the skeleton is worth
- little until that is costed.
-
-## A naming near-miss, recorded
-
-The tier-0 binary was first called `mesh-agent`, because "node agent" is the reflex everywhere
-else in the industry. That is wrong here, and wrong in the specific way
-[`how-we-build.md`](../../00-META/how-we-build.md) §4 exists to catch: **Agent** is a
-first-class concept in this mesh — a participant, some of whom are human, holding identity and
-memory ([ADR 0003](../../02-DECISIONS/0003-agents-are-persistent-employees.md)). One document
-carried both meanings.
-
-It is the same failure as the anatomy naming in the current runtime: an evocative domain word
-pointing at infrastructure.
-
-[ADR 0001](../../02-DECISIONS/0001-mesh-brokers-nodes-host-agents-think.md) supplies the fix in
-its own title — *the mesh brokers capabilities; nodes host; agents think.* Three verbs, three
-components: the control plane **brokers** (`mesh-control`), the tier-0 binary **hosts**
-(`mesh-host`), the participant **thinks** (`agents`, untouched).
-
-`mesh-node` was the alternative and was rejected: *Node* is the aggregate in the inventory — the
-record of a machine — while the binary is what runs on it and does the hosting.
diff --git a/01-RESEARCH/007-provisioning-as-the-core/00-overview.md b/01-RESEARCH/007-provisioning-as-the-core/00-overview.md
deleted file mode 100644
index 6c12de99..00000000
--- a/01-RESEARCH/007-provisioning-as-the-core/00-overview.md
+++ /dev/null
@@ -1,75 +0,0 @@
----
-status: graduated
-initiated: 2026-08-23
-touches:
- - 03-DESIGN/00-as-is/03-provisioning.md
- - 02-DECISIONS/0009-modules-and-the-graph.md
- - 01-RESEARCH/006-mesh-from-scratch/code-skeleton.md
-became:
- - 02-DECISIONS/0028-the-substrate-supplies-the-control-plane-and-nothing-else.md
- - 02-DECISIONS/0033-the-substrate-is-a-store-and-a-broker.md
- - 02-DECISIONS/0078-the-store-and-broker-are-modules.md
- - 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
- - 02-DECISIONS/0114-a-shared-credential-rotates-over-two-credentials.md
- - 02-DECISIONS/0144-anything-on-a-machine-may-call-anything-on-it.md
- - 02-DECISIONS/0224-a-provider-that-keeps-failing-a-consumer-is-a-problem-the-controller-reports.md
- - 03-DESIGN/01-to-be/13-credentials-and-their-rotation.md
- - 03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md
----
-
-# 007 — Provisioning as the mesh's core mechanism
-
-## What is being investigated
-
-Provisioning is the mechanism the whole mesh rests on: a module declares what it needs, and the
-mesh makes it exist, generates the credential, records the grant, and puts the values where the
-module will read them. [ADR 0009](../../02-DECISIONS/0009-modules-and-the-graph.md)
-calls it the mesh's core concern rather than its plumbing.
-
-[Research 006](../006-mesh-from-scratch/code-skeleton.md) then asks it to carry **more**: the
-control plane becomes a consumer with its own requirements — a source of record, an image
-registry, a package registry — satisfied by the same mechanism. That generalisation is only
-safe if the mechanism is sound, and the as-is record says it is not, in named ways.
-
-## Why now
-
-Four weaknesses are already documented in
-[`03-DESIGN/00-as-is/03-provisioning.md`](../../03-DESIGN/00-as-is/03-provisioning.md), each
-observed rather than theorised:
-
-- **Rotation has no fan-out.** A shared credential can be rotated without telling the peers
- holding the old one. This has locked the mesh out of its own broker.
-- **A grant is not a check.** The record says a resource was provisioned. Nothing verifies it
- still exists, still has that credential, or is reachable from where the consumer runs.
-- **A frozen password outlives its generation.** A generated secret written once diverges from a
- persistent data directory initialised earlier, and presents as an authentication error.
-- **No requirements is indistinguishable from provisioning that did not run.** A module that
- declares nothing skips the stage, which is correct, and looks identical to failure.
-
-Generalising a mechanism with these properties to the control plane's own dependencies would
-make each of them fatal rather than annoying.
-
-## The questions
-
-| Question | Why it matters |
-|---|---|
-| What does a **grant** mean, exactly — a record that a resource was created, or a claim about the world that is continuously reconciled? | The difference between the current model and one where "provisioned" is checkable. Almost every weakness above is a symptom of the first answer. |
-| How is a credential **rotated** with fan-out to every holder? | The mechanism grants easily and regrants not at all. This is the most damaging gap and it has taken the mesh down. |
-| Can the **control plane** hold requirements, and what satisfies them before anything is installed? | The generalisation research 006 needs. Ties directly to the self-hosting transition. |
-| What happens when a requirement **cannot** be satisfied — no provider, provider on an unreachable node, provider not yet installed? | Today this is silent or a stall. It should be a stated, visible state. |
-| Does a requirement belong to a **module** or to one of its **parts**? | Research 006 splits `feature` into artifact and part. A part-scoped requirement means a database is not provisioned where the part that needs it is not installed. |
-
-## What it became
-
-*Closed 2026-10-10.* All five questions have an accepted answer.
-
-- **What a grant means**: the provider checks each consumer periodically and announces one it keeps failing,
- which the controller reports ([ADR 0224](../../02-DECISIONS/0224-a-provider-that-keeps-failing-a-consumer-is-a-problem-the-controller-reports.md)). A consumer verifying its own grant was proposed and withdrawn the
- same day ([ADR 0144](../../02-DECISIONS/0144-anything-on-a-machine-may-call-anything-on-it.md)).
-- **Rotation with fan-out**: [ADR 0114](../../02-DECISIONS/0114-a-shared-credential-rotates-over-two-credentials.md), designed in [to-be 13](../../03-DESIGN/01-to-be/13-credentials-and-their-rotation.md).
-- **The controller's own requirements**: the foundation is what the controller needs and cannot grant itself
- ([ADR 0028](../../02-DECISIONS/0028-the-substrate-supplies-the-control-plane-and-nothing-else.md), [ADR 0033](../../02-DECISIONS/0033-the-substrate-is-a-store-and-a-broker.md), [ADR 0078](../../02-DECISIONS/0078-the-store-and-broker-are-modules.md)).
-- **A requirement nobody can answer**: installing a module resolves every requirement or is refused, naming
- each one ([ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md), designed in [to-be 27](../../03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md)).
-- **Module or part**: everything a module needs is that module's requirement ([ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md)); a module is
- not split into separately selected parts.
diff --git a/01-RESEARCH/008-delivery-coordinator/00-overview.md b/01-RESEARCH/008-delivery-coordinator/00-overview.md
deleted file mode 100644
index 21ec3b7c..00000000
--- a/01-RESEARCH/008-delivery-coordinator/00-overview.md
+++ /dev/null
@@ -1,101 +0,0 @@
----
-status: graduated
-initiated: 2026-08-23
-touches:
- - 03-DESIGN/00-as-is/04-delivery.md
- - 02-DECISIONS/0010-delivery.md
- - 02-DECISIONS/0010-delivery.md
- - 01-RESEARCH/006-mesh-from-scratch/code-skeleton.md
-became:
- - 02-DECISIONS/0010-delivery.md
- - 02-DECISIONS/0010-delivery.md
----
-
-# 008 — The coordinator: a change checked in becomes a deployed state
-
-## What is being investigated
-
-The mesh's own continuous delivery: a change is committed, and the mesh ends up in the state
-that change describes — across every node the change touches, with a verdict that says whether
-it worked.
-
-The coordinator is what orchestrates that, and it is the mesh's most consequential machinery:
-everything reaches every node through it.
-
-## Why now
-
-The as-is record ([`03-DESIGN/00-as-is/04-delivery.md`](../../03-DESIGN/00-as-is/04-delivery.md))
-names problems that are structural rather than incidental:
-
-- **A green pipeline proves transport, not effect.** The stages report that a message was
- dispatched and accepted, which is not the same as the thing running, correct, or present.
- This is the mesh's single most consistent failure shape.
-- **Detection is the most fragile input.** A merge that creates no pipeline, with nothing saying
- so, is the characteristic bad outcome — and it has happened for reasons unrelated to the
- change.
-- **The fan-out point is asymmetric.** The build node has already passed two silos when work
- fans out, and code that knew only about the first parked it forever while every other node
- deployed cleanly.
-- **There is no end-to-end coverage.** The harness has not built since 2026-06-04
- ([`04-ISSUES/005`](../../04-ISSUES/005-pipeline-test-harness-unbuildable/00-report.md)).
-
-Research 006 adds a requirement the current design does not have: the coordinator must work
-**before the mesh is self-hosting**, when source and artifacts come from outside, and keep
-working across the transition to self-hosted providers.
-
-## What it became
-
-*Closed 2026-08-28.* All six questions are answered, by two records, and the second exists
-because the first was honest about what it did not fix.
-
-**Does the coordinator dispatch stages, or converge nodes on a declaration?** — *Converge.*
-[ADR 0010](../../02-DECISIONS/0010-delivery.md): a pipeline ends when the
-declaration is updated, and the host applies it and reads back — so the reporter is the applier.
-
-**Does the three-silo split survive?** — *Yes, with the third redefined.* The cardinality
-observation holds; the third silo is not a stage any more.
-
-**How does a change become a pipeline, reliably?** — *It does not become a pipeline at all.*
-[ADR 0010](../../02-DECISIONS/0010-delivery.md) applies 0058's
-move one level up: the control plane holds what source exists and what has been built, and builds
-the difference. **An event makes it fast; nothing makes it necessary.** The failures this effort
-catalogued — a truncated commit list, a broken path match — become latency rather than silence.
-
-**What is a deployed state?** — *Two comparisons, not an event.* Does every node's reported state
-match what is declared, and is what is declared built from current source? A milestone can be
-claimed by something that did not check; a comparison cannot.
-
-**What produces a verdict, and what is it about?** — *An artifact, and it gates eligibility.* The
-mesh must not converge onto something broken, so an artifact may be declared only once the lab
-has judged it fit. Sharper than the question expected: a verdict is a property an artifact has,
-not a report about a run.
-
-**How does delivery work before self-hosting?** — *It mostly stops being a question.* A reconciler
-reads source and writes artifacts; where those live is a binding, external first and internal
-later. The transition looked hard because a pipeline's stages name their targets.
-
-## What this effort was right about
-
-Its first question — *what is a deployed state, and how does the mesh know it is in one* — was
-marked "everything follows from this", and everything did. Both records above are answers to it:
-0058 makes the applier the reporter, and 0063 makes currency a comparison. The effort put the
-load-bearing question first.
-
-## What is NOT closed by this
-
-[ADR 0010](../../02-DECISIONS/0010-delivery.md) names four costs
-and one of them is a real risk rather than a trade: **a reconciler that cannot reach its target
-retries forever, and without something that notices, the failure is silence** — which is the
-fault this effort exists to catalogue, reintroduced in a new place. That belongs to observability
-and it is not designed.
-
-## The questions (all answered above)
-
-| Question | Why it matters |
-|---|---|
-| What is a **deployed state**, and how does the mesh know it is in one? | Everything follows from this. If a stage reports transport, "deployed" is a claim nobody checked. A desired-state model with reconciliation gives a different answer from a job-completion model. |
-| Does the coordinator dispatch **stages**, or converge nodes on a **declaration**? | The current model is a state machine over stages. The alternative is that a node is told what should be true and reports what is. The second makes drift visible; the first cannot see it. |
-| How does a change **become** a pipeline, reliably? | Detection has failed for reasons unrelated to the change, silently. |
-| What produces a **verdict**, and what is it a verdict about? | Ties to the lab ([ADR 0016](../../02-DECISIONS/0016-the-lab.md)) and to a module carrying its own assertions. |
-| How does delivery work **before self-hosting**, and across the transition? | From research 006: source and artifacts start external and are re-bound to internal providers. The coordinator has to be indifferent to which. |
-| Does the **three-silo** split survive the artifact/part split? | [ADR 0010](../../02-DECISIONS/0010-delivery.md) is cardinality-driven, and research 006 renames the thing the cardinality is about. |
diff --git a/01-RESEARCH/009-migration/00-overview.md b/01-RESEARCH/009-migration/00-overview.md
deleted file mode 100644
index 4e7f9fc1..00000000
--- a/01-RESEARCH/009-migration/00-overview.md
+++ /dev/null
@@ -1,109 +0,0 @@
----
-status: abandoned
-initiated: 2026-08-23
-touches:
- - 01-RESEARCH/006-mesh-from-scratch/code-skeleton.md
- - 03-DESIGN/01-to-be/01-end-to-end-testing.md
- - 02-DECISIONS/0016-the-lab.md
- - 03-DESIGN/00-as-is/00-overview.md
-became:
- - 02-DECISIONS/0067-genesis-is-a-pivot.md
- - 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md
- - 02-DECISIONS/0105-the-mesh-adopts-the-predecessors-tunnel-in-place.md
- - 02-DECISIONS/0119-a-taken-tunnels-predecessor-is-retired.md
- - 02-DECISIONS/0173-the-operators-machine-is-the-meshs-and-a-module-is-what-it-declares.md
- - 02-DECISIONS/0200-genesis-pivots-to-the-controller-as-a-container-and-the-first-push-hands-it-to-a-process.md
----
-
-# 009 — Getting from the mesh that exists to the mesh that is designed
-
-## What is being investigated
-
-How a running mesh becomes the one in
-[research 006](../006-mesh-from-scratch/code-skeleton.md), without losing what it currently
-carries.
-
-The proposed shape: **build tiers 0, 1 and 2, then replace the current setup in one move.**
-
-## Why big-bang is the right instinct here
-
-Recorded because incremental is the reflex answer and it is wrong in this case.
-
-- **The two models are structurally incompatible.** The tier rule, the host absorbing what are
- now modules, the artifact/part split, provisioning generalised to the control plane's own
- requirements — none of these can half-apply. Running both models at once means the old one's
- assumptions keep constraining the new one, which is how a migration becomes permanent.
-- **Nothing external depends on it.** No users outside the operator, no service level to hold.
-- **The lab exists precisely for this** ([ADR 0016](../../02-DECISIONS/0016-the-lab.md)).
- A big-bang that has been rehearsed end to end, repeatedly, on identical machines is not the
- same risk as one performed for the first time on the real mesh. This is also why the lab is
- phase 0 rather than a verification step later: the new mesh is *developed* inside it, so by
- cutover the procedure has been run hundreds of times rather than rehearsed a few.
-- **Incremental would carry the rot forward.** The as-is layer documents silent failure paths,
- a dead test harness and unenforced rules. A gradual migration preserves them by definition.
-
-## The distinction that lowers the risk
-
-**Replace the control plane; do not move the workloads.**
-
-The things that would hurt to lose — mail, media, source, databases, their data directories —
-are not the mesh. They are what the mesh manages. They sit in container volumes on nodes, and
-they do not need to move for the control plane above them to be replaced.
-
-So the big-bang is: the old control plane stops managing these nodes, and the new one starts —
-with the workload data untouched, in place, and re-declared rather than migrated.
-
-That reframing turns "replace the mesh" into "replace the part with no persistent state of its
-own", which is a materially smaller act than it first sounds.
-
-## The tension this exposes
-
-Taking over already-running workloads is **adoption**, and adoption was ruled out of scope —
-recorded as a legacy path in
-[`03-DESIGN/01-to-be/01-end-to-end-testing.md`](../../03-DESIGN/01-to-be/01-end-to-end-testing.md).
-The migration appears to need exactly the capability the design declared it would not have.
-
-The way out, to be tested: the new mesh does not adopt anything. It **declares** the workloads
-from scratch and points them at data directories that already exist. Nothing inspects a running
-machine to learn what is there; the declarations are written from the as-is layer, which is what
-that layer is for. Data survives because it was never touched, not because it was adopted.
-
-If that holds, adoption stays out of scope and the migration is ordinary declaration. If it does
-not, adoption needs a one-time, explicitly unsupported tool, and that should be a decision rather
-than a discovery.
-
-## Sequencing
-
-| Phase | What | Done when |
-|---|---|---|
-| **0** | **Build the lab's bootstrap scenario** ([ADR 0016](../../02-DECISIONS/0016-the-lab.md)) — virtual machines, a network, a way to place a binary, snapshot and reset. No forge, no coordinator, no pipeline. | A machine can be raised from nothing, reset, and raised again, repeatably. |
-| A | Build tier 0, **inside the lab**. The host's interface first — it carries the skeleton's biggest unproven claim. | A bare machine becomes a managed node with no mesh present. |
-| B | Build tier 1 and 2. The bootstrap scenario grows into the full one by addition — the same machines, with more placed inside them. | The lab raises a full mesh from nothing, repeatedly, from pinned external artifacts. |
-| C | Enough of tier 3 to operate it. | The mesh can be driven without direct database access. |
-| D | Declare the existing workloads against the new model. | The lab runs them, with copies of real data shapes. |
-| E | **Rehearse the cutover in the lab** against a mesh built to resemble the real one. | Repeatable, and repeatably reversible. |
-| F | Cut over. | The real nodes are managed by the new control plane. |
-| G | Reach self-hosting — re-bind delivery from external providers to the mesh's own forge and registries. | The mesh builds and deploys itself. |
-
-Phase G is deliberately last. Per research 006, self-hosting is a state the mesh **reaches**;
-attempting the cutover and the self-hosting transition in the same move recreates exactly the
-circularity the skeleton removes — and would mean a failed cutover could take away the means to
-fix it.
-
-## The questions
-
-| Question | Why it matters |
-|---|---|
-| What state must **survive** the cutover, versus be re-created? | Workload data must. Provisioned credentials could be re-issued. The mesh's own inventory could be re-declared. Each answer changes the risk. |
-| What is the **way back**? | A cutover with no rollback is not a plan. If workload data is untouched, reverting may be as small as re-pointing the old control plane at it — to be verified, not assumed. |
-| How is the cutover **rehearsed** against something resembling the real mesh, without copying the real mesh into a repository? | The lab must be able to model the real topology's shape without carrying its identity. |
-| Does anything have to keep running **during** the cutover? | Mail and source are the obvious candidates. If yes, "big-bang" is really "big-bang with exceptions", and the exceptions should be named now. |
-| Is the forge inside or outside the cutover? | If the mesh's own forge goes down with the old control plane, the means of deploying a fix goes with it. This is the self-hosting circularity appearing as a migration risk. |
-
-## What overtook it
-
-*Closed 2026-10-10.* The one-move switch-over was never made. The migration went machine by machine instead: a machine in use
-was adopted first and converged module by module ([ADR 0100](../../02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md)), the predecessor's tunnel was adopted in
-place and then retired ([ADR 0105](../../02-DECISIONS/0105-the-mesh-adopts-the-predecessors-tunnel-in-place.md), [ADR 0119](../../02-DECISIONS/0119-a-taken-tunnels-predecessor-is-retired.md)), genesis became a pivot from a running mesh
-([ADR 0067](../../02-DECISIONS/0067-genesis-is-a-pivot.md), [ADR 0200](../../02-DECISIONS/0200-genesis-pivots-to-the-controller-as-a-container-and-the-first-push-hands-it-to-a-process.md)), and the predecessor is retired from the operator's machines ([ADR 0173](../../02-DECISIONS/0173-the-operators-machine-is-the-meshs-and-a-module-is-what-it-declares.md)).
-That migration is finished.
diff --git a/01-RESEARCH/010-lab-inner-loop-cost/00-overview.md b/01-RESEARCH/010-lab-inner-loop-cost/00-overview.md
deleted file mode 100644
index e66237ff..00000000
--- a/01-RESEARCH/010-lab-inner-loop-cost/00-overview.md
+++ /dev/null
@@ -1,73 +0,0 @@
----
-status: graduated
-initiated: 2026-08-24
-touches:
- - 03-DESIGN/01-to-be/03-scenario-lifecycle.md
- - 02-DECISIONS/0016-the-lab.md
- - 02-DECISIONS/0016-the-lab.md
-became:
- - 02-DECISIONS/0016-the-lab.md
- - 02-DECISIONS/0172-the-lab-is-a-module-and-runs-a-bed-when-the-mesh-asks.md
- - 03-DESIGN/01-to-be/03-scenario-lifecycle.md
- - 03-DESIGN/01-to-be/04-lab-installation.md
----
-
-# 010 — What the lab's inner loop actually costs
-
-## What is being investigated
-
-[The lifecycle design](../../03-DESIGN/01-to-be/03-scenario-lifecycle.md) closes on an open
-question that is measurable rather than arguable:
-
-> **What a snapshot costs.** Whole-scenario snapshots of several machines are the operation the
-> inner loop repeats most, so their cost sets the loop's speed. If restoring is slow, the loop
-> is slow, and everything above is theory.
-
-Measured on a workstation, 2026-08-24. Numbers in [`measurements.md`](measurements.md).
-
-## Why it matters
-
-[ADR 0016](../../02-DECISIONS/0016-the-lab.md) makes the
-bootstrap scenario the inner development loop for tiers 0 and 1 — the argument being that
-raising a node from nothing stops being the least-exercised path and becomes the most-exercised
-one. **That argument is only true if raising and resetting are cheap.** A loop that costs
-minutes is a loop people avoid, and the least-exercised path stays least-exercised.
-
-## Status
-
-Measured, and the finding is a blocker rather than a data point.
-
-**A snapshot on the current host is a full copy of the machine's disk.** 1.6 GB and ten seconds
-for one small virtual machine at best — and over two minutes when observed a second time. Cost
-scales with the number of machines and the size of their disks, not with what changed.
-
-**With copy-on-write it is 0.13 seconds and costs the delta.** Verified, not assumed. The
-projected four-machine reset cycle falls from roughly ninety seconds to roughly fifteen, of
-which almost all is a boot that cannot be avoided.
-
-The cause is not virtual machines and not incus. It is that the host offers incus exactly one
-storage driver, `dir`, which has no copy-on-write and therefore no cheap snapshot. The kernel
-supports btrfs; the userspace tool that would let incus use it is simply not installed.
-
-So the question *"is the lab's inner loop fast enough"* currently answers itself the wrong way,
-for a reason that is one declared package away from being fixed — and declaring packages is
-something the mesh already does.
-
-## Open questions
-
-| Question | Why it matters |
-|---|---|
-| ~~How much does a copy-on-write pool actually improve it?~~ **Measured: snapshot 9.9 s → 0.13 s, restore 10.4 s → 0.80 s, three snapshots sharing 1.36 GB rather than costing 4.8 GB.** The projected four-machine cycle falls from ~90 s to ~15 s. | Answered. The inner-loop argument holds *with* copy-on-write and did not without it. |
-| How does the lab install its own prerequisites on a clean machine? | The lab needs a virtualisation daemon, copy-on-write tooling and a pool before it can do anything — and it cannot depend on the mesh for them, since it is where the mesh is built. |
-| Why was the second snapshot more than twelve times slower than the first? | If snapshot cost is unpredictable rather than merely high, that is worse — a loop with a variable multi-minute step is one nobody trusts. |
-| Does a scenario snapshot need the machines stopped? | Stateless snapshots of a running virtual machine capture the disk but not memory. Whether a mesh restored that way is coherent is not established. |
-| What is the cost at scenario scale — four machines rather than one? | Only single-machine numbers were taken. If the operation is serial, four machines is four times the wait. |
-
-## What it became
-
-*Closed 2026-10-10.* The cost was measured and the answer is built in. The lab's own installation requires a copy-on-write
-pool and refuses a machine without one ([to-be 04](../../03-DESIGN/01-to-be/04-lab-installation.md), [ADR 0172](../../02-DECISIONS/0172-the-lab-is-a-module-and-runs-a-bed-when-the-mesh-asks.md)), and the lifecycle design
-([to-be 03](../../03-DESIGN/01-to-be/03-scenario-lifecycle.md), under [ADR 0016](../../02-DECISIONS/0016-the-lab.md)) rests on the measured numbers. Two machines were measured as well as one,
-and boot concurrently ([as-is 11](../../03-DESIGN/00-as-is/11-the-lab.md)). The integration test answered
-whether a snapshot needs its machines stopped: no, but they must be flushed first ([to-be 03](../../03-DESIGN/01-to-be/03-scenario-lifecycle.md)).
-Why the second full-copy snapshot was slower stopped mattering once snapshots were copy-on-write.
diff --git a/01-RESEARCH/010-lab-inner-loop-cost/measurements.md b/01-RESEARCH/010-lab-inner-loop-cost/measurements.md
deleted file mode 100644
index ee3d9a86..00000000
--- a/01-RESEARCH/010-lab-inner-loop-cost/measurements.md
+++ /dev/null
@@ -1,141 +0,0 @@
----
-effort: 010-lab-inner-loop-cost
-updated: 2026-08-24
----
-
-# Measurements
-
-Taken 2026-08-24 on a workstation with hardware virtualisation available, an NVMe-backed ext4
-root, and 300 GB free. One virtual machine, 1 GiB memory, 2 CPUs, from a cached distribution
-image.
-
-## The environment, before anything ran
-
-| Fact | Value | Consequence |
-|---|---|---|
-| Hardware virtualisation | present | virtual machines run at native speed; the choice in [ADR 0016](../../02-DECISIONS/0016-the-lab.md) is not paying an emulation penalty |
-| Storage drivers the daemon offers | **`dir` only** | no copy-on-write, therefore no cheap snapshot |
-| Host filesystems | ext4 throughout | nothing copy-on-write to put a pool on |
-| btrfs kernel module | **available** | the kernel can do it |
-| `btrfs-progs` | **not installed** | which is the entire reason the driver is absent |
-
-The last two rows are the finding. The daemon advertises only `dir` because the userspace tool
-for anything better is missing — not because the host cannot do better.
-
-## Raising a machine
-
-| Step | Time |
-|---|---|
-| launch call returns | **3.4 s** |
-| machine actually usable — a command executes on it | **14.3 s** |
-
-The gap matters for the lifecycle design: `raise` returning is not the same as the scenario
-being ready, so the verb has to wait for the second number, not report the first. Reporting
-the first would be the mesh's own recurring failure — transport reported as effect.
-
-## Snapshot and restore
-
-| Operation | Time | Disk |
-|---|---|---|
-| snapshot, first | **9.9 s** | **+1.6 GB** |
-| snapshot, second | **> 120 s — did not complete** | — |
-| restore call returns | **10.4 s** | — |
-| machine usable again | **20.1 s** total | — |
-
-Instance on disk before snapshotting: 1.5 GB. Snapshot directory afterwards: 1.6 GB. **A `dir`
-snapshot is a full copy** — the storage cost equals the instance, and nothing is shared.
-
-Implied copy throughput on the first snapshot is roughly 160 MB/s, which is far below what the
-underlying NVMe can do and is consistent with a real, durable copy rather than a metadata
-operation.
-
-**The second snapshot is the more troubling number.** It exceeded two minutes and was still
-running when the observation was cut off; only the first snapshot exists. Whatever the cause —
-page cache exhausted by the preceding restore, writeback contention — the practical
-consequence is that snapshot cost here is **not merely high, it is unpredictable**.
-
-## What this projects to
-
-A four-machine scenario, taking the optimistic single-machine numbers and assuming the
-operations are serial:
-
-| | one machine | four machines |
-|---|---|---|
-| raise, to usable | 14 s | ~57 s |
-| snapshot | 10 s, 1.6 GB | ~40 s, 6.4 GB |
-| restore, to usable | 20 s | ~80 s |
-
-A reset-and-rerun cycle is therefore **around a minute and a half at best**, and unbounded at
-worst, before any of the mesh's own work begins.
-
-## The judgement
-
-**This is too slow for an inner loop**, and the reason is not the design.
-
-[ADR 0016](../../02-DECISIONS/0016-the-lab.md) argues that
-making the bootstrap path the inner development loop turns the least-exercised code in the
-system into the most-exercised. That argument holds only while resetting is cheap. At a minute
-and a half a cycle, with occasional multi-minute stalls, the loop is one a person works around
-— and the path stays under-exercised for exactly the reason it always was.
-
-Nothing about virtual machines causes this. Hardware virtualisation is present and the machines
-boot in fourteen seconds. **The cost is entirely the storage driver**, and the driver is absent
-because one userspace package is not installed on the host.
-
-The mesh already has the mechanism for that: a module declares a package, and a hook makes it a
-working capability — which is precisely what was just done for the virtualisation daemon
-itself, and what [`04-ISSUES/007`](../../04-ISSUES/007-an-installed-package-is-not-a-capability/00-report.md)
-is about.
-
-## The fix, measured
-
-The comparison was subsequently run. One package — `btrfs-progs`, no dependencies — installed by
-hand, the daemon restarted so it re-detected drivers, a copy-on-write pool created on a loop
-file, and the identical image launched onto it.
-
-| Operation | `dir` | copy-on-write | |
-|---|---|---|---|
-| snapshot | 9.9 s, then **> 120 s** | **0.13 s** | ~76× faster, and *consistent* |
-| snapshot again | — | 0.12 s | |
-| snapshot a third time | — | 0.13 s | |
-| restore call | 10.4 s | **0.80 s** | ~13× faster |
-| restore, to usable | 20.1 s | **10.5 s** | the remainder is boot, which is irreducible |
-| three snapshots, storage | ~4.8 GB | **1.36 GB total, shared** | cost is the delta, not the disk |
-
-**The fix is real, and larger than expected.** Snapshot goes from ten seconds to a tenth of a
-second, and — more importantly — from *wildly variable* to *flat*. Three consecutive snapshots
-took 0.13, 0.12 and 0.13 seconds. On `dir` the second snapshot never finished.
-
-Storage stops scaling with the machine and starts scaling with what changed: three snapshots of
-a 1.5 GB instance occupied 1.36 GB in total, because they share.
-
-### What it projects to
-
-A four-machine reset-and-rerun cycle, the operation the inner loop repeats most:
-
-| | `dir` | copy-on-write |
-|---|---|---|
-| snapshot the scenario | ~40 s, 6.4 GB | **~0.5 s**, delta-sized |
-| restore it | ~40 s + boot | **~3 s** + boot |
-| **cycle** | **~90 s, unbounded at worst** | **~15 s, dominated by boot** |
-
-At fifteen seconds, dominated by a boot that cannot be avoided, the inner loop is viable and
-[ADR 0016](../../02-DECISIONS/0016-the-lab.md)'s argument holds.
-At ninety it did not.
-
-### One honest counter-observation
-
-Launching onto the fresh copy-on-write pool was **slower** — 20.2 s to usable against 14.3 s —
-because the image had to be unpacked into a pool that had never seen it. That cost is paid once
-per pool, not per scenario, and it is dwarfed by what snapshotting saves. But it is a real
-number and it went the other way.
-
-### State this left behind
-
-Recorded because hand-made state is exactly what the mesh's rules exist to prevent, and it must
-be declared properly rather than left as an artefact of a measurement:
-
-- `btrfs-progs` installed by hand. Its installation regenerated the boot initramfs, a side
- effect worth knowing about.
-- The daemon restarted once, to re-detect drivers.
-- The test pool and instance were **removed**; the pool the lab actually needs does not exist.
diff --git a/01-RESEARCH/011-the-module-graph/00-overview.md b/01-RESEARCH/011-the-module-graph/00-overview.md
deleted file mode 100644
index 6a77445e..00000000
--- a/01-RESEARCH/011-the-module-graph/00-overview.md
+++ /dev/null
@@ -1,203 +0,0 @@
----
-status: graduated
-initiated: 2026-08-25
-became:
- - 02-DECISIONS/0009-modules-and-the-graph.md
- - 02-DECISIONS/0008-a-context-owns-its-store.md
- - 03-DESIGN/01-to-be/06-the-controller.md
- - 03-DESIGN/01-to-be/07-the-foundation.md
-touches:
- - 02-DECISIONS/0009-modules-and-the-graph.md
- - 02-DECISIONS/0009-modules-and-the-graph.md
- - 02-DECISIONS/0004-a-node-and-how-it-joins.md
- - 03-DESIGN/00-as-is/02-modules-and-manifests.md
- - 03-DESIGN/00-as-is/10-module-catalogue.md
- - 04-ISSUES/007-an-installed-package-is-not-a-capability/00-report.md
----
-
-# 011 — The module graph
-
-> **A third edge was found after this graduated.** This effort established *presence* and
-> *instantiation*, and both are **runtime** edges — they answer *what does this need in order to
-> run*. Delivery needs a different question answered — *what has to be rebuilt when this changes*
-> — and that is a **build** edge, fixed inside an artifact rather than negotiated when it runs.
-> Recorded by [ADR 0009](../../02-DECISIONS/0009-modules-and-the-graph.md), which also
-> notes what this effort's three entities turn out to be good for
-> ([ADR 0009](../../02-DECISIONS/0009-modules-and-the-graph.md)).
-
-## What is being investigated
-
-Whether the catalogue's missing structure is a **graph** — modules declaring what they need,
-what they offer, and what they exclude — and what that replaces.
-
-**[`worked-provider.md`](worked-provider.md) works one module through completely**, and breaks
-the tidy version. A database is nine things, not one — and a small game asking the mesh for its
-own database shows there are **two kinds of edge**: *presence*, where the thing must exist, and
-*instantiation*, where a provider makes something for a consumer and hands back credentials.
-Instantiation implies presence and not the reverse. The current system already had this split
-and the design had collapsed it.
-
-**[`features.md`](features.md) answers what happens to `feature`.** It is one word for four
-things spanning three tiers — artifacts built once per version, resources applied to a machine,
-actions run against something that is not this machine, and checks that are requirements in
-disguise. Measured: every one of the twenty-one handlers implements all six stages, so `configs`
-has a build stage with nothing to build and `npm` has a start stage with nothing to start. That
-emptiness is the conflation, and it is why a stage that did nothing and a stage that failed look
-alike. Nothing replaces it, because it was never one concept. **One property is worth keeping:
-content is detected, relationships are declared.**
-
-**[`cases.md`](cases.md) enumerates what a module can be** — twenty kinds of thing the mesh has
-to install, run, own or know about — and extracts the axes a manifest must express. Two of those
-axes appear in no current thinking: **how many instances** a thing may have, and **whether two
-can coexist**.
-
-**The design is in [`proposal.md`](proposal.md): one kind of edge.** A module provides names
-and requires names, and that single relation absorbs requiring a module, requiring a resource,
-and the interface-and-adapter idea. An abstract name is legitimate **only where providers are
-genuinely substitutable** — `terminal` passes, `database` does not, because a consumer speaking
-Postgres does not speak MongoDB. The adapter is what creates an interface; without one there is
-a **tag**, which describes and does not bind.
-A **node provides names too**, which makes capability checking stop being a separate mechanism
-and makes the host's own capability report an input to resolution rather than something a
-person reads.
-
-[`analysis.md`](analysis.md) measured the current catalogue. Its value to the design is two
-lessons rather than its machinery — a field that means *depends on* should say so, and
-placement does not belong in a manifest — and the rest is recorded as as-is evidence.
-
-**Measured, and the premise was wrong for the existing system: the graph is not missing there.**
-[`analysis.md`](analysis.md) — 126 manifests, 103 edges, no cycles, nothing dangling, and a
-resolver that topologically sorts them, already called by the tool loader, the installer and
-the delivery coordinator. What the effort assumed would need building is a thing to call.
-
-What survives is narrower: three declarations that do not exist (`excludes`, a required node
-capability, an interface with adapters), and two defects worth fixing whatever else is
-concluded — `provider:` is a dependency edge that is not read as one, which makes the closure
-for a working mesh come out without a database; and the resolver continues past a cycle and
-past a missing dependency, contrary to ADR 0001.
-
-[ADR 0009](../../02-DECISIONS/0009-modules-and-the-graph.md) proposes
-grouping modules by domain. [Research 005](../005-domain-grouping/analysis.md) measured that
-proposal and found its evidence holds in exactly one place — reachability — which
-[ADR 0005](../../02-DECISIONS/0005-the-node-host.md) has since absorbed
-into the host. The measured case for domain grouping has therefore been consumed by a decision
-taken for unrelated reasons, and what remains is fifty modules that co-change with nothing.
-
-That leaves the original complaint unanswered: the catalogue records **what was installed**
-rather than **what anything is for**, and nothing in the system can see a relationship between
-two modules. Grouping asserts relationships. A graph records them.
-
-## Why now
-
-A proposal to split modules into *provisioning services* and *applications* was worked through
-and abandoned in favour of one concept with facets, for a reason worth keeping:
-
-- The split cannot be filed consistently. A git forge is consumed as a service *and* operated
- through a web interface. An analytics service grants tracking identity *and* is a dashboard
- somebody reads. An identity provider grants authentication *and* has an admin console.
-- The operator's correction is the sharper form: **what runs on the machine is a supervised
- container, not something a user started.** That is a fact about *how a thing runs*, not about
- what kind of thing it is — so it is a facet, not a taxonomy.
-
-Filing decisions that follow from nothing are the disease research 005 measured. A second
-taxonomy would reproduce it.
-
-So [ADR 0009](../../02-DECISIONS/0009-modules-and-the-graph.md) survives, and the question
-becomes what a module must be able to **declare**.
-
-## The shape being investigated
-
-Five declarations, of which two exist today.
-
-| Declaration | Today | Notes |
-|---|---|---|
-| **requires a resource** — a database, a bucket | yes | provisioning, [ADR 0009](../../02-DECISIONS/0009-modules-and-the-graph.md) |
-| **provides a resource** | yes | as above |
-| **requires another module** | **no** | the dependency edge — the graph's substance |
-| **excludes another module** | **no** | installing A makes B unavailable |
-| **requires a node capability** | **no** | a graphical session, a container runtime, an architecture |
-
-And one structural idea on top: **an interface module carries no implementation.** Adapters
-provide it. An assistant interface with several model-provider adapters; a terminal interface
-with several terminal adapters. A dependent names the interface and never an implementation.
-
-**Prior art to measure against, not invent past.** This is a package manager's model, and the
-platform's own package manager already has all of it: `depends` is the dependency edge,
-`conflicts` is exclusion, and `provides` is the interface — several packages provide one
-virtual name, and a dependent names the virtual one. That the design arrived at the same shape
-independently is evidence for it. It is also a warning: dependency resolution, version
-constraints, conflict handling and rollback are a long-solved and easily-botched problem, and
-the effort should establish what to **delegate** rather than reimplement.
-
-## Capabilities, and what may be installed
-
-The operator's formulation: *system specs are capabilities, and capabilities unlock installable
-modules — you cannot install a graphical application on a node with no display server.*
-
-The question that follows is whether the mesh may install a capability. The effort's working
-position, to be tested:
-
-- **Intrinsic capabilities** — hardware, architecture, network position — are facts about a
- machine. They are detected, never installed, and a module requiring one it does not have is
- not unresolved but **impossible** on that node.
-- **Provided capabilities** — a display server, a container runtime — are not a separate kind
- of thing at all. They are modules that provide a capability, and requiring one is an ordinary
- edge the graph resolves by installing it.
-
-If that holds, "may the mesh install a capability" is not a policy question. It is dependency
-resolution, and the only genuinely new thing is detection.
-
-Which is where [issue 007](../../04-ISSUES/007-an-installed-package-is-not-a-capability/00-report.md)
-bears directly: *an installed package is not a capability*. A provided capability is not real
-because a package is present — it is real when it is present, running and working, and the
-difference is exactly the class of fault this repository keeps recording.
-
-## What it touches beyond the catalogue
-
-The operator's assessment is that the machinery around a module is wanted and its integration
-is not: **scheduled tasks, hooks, migrations, configuration and environment settings are worth
-keeping; seeds are not; and the current integration is wrong enough to need a major refactor.**
-
-That is a claim to test rather than adopt. Research 005 already found supporting evidence from
-a different direction — that the densest apparent coupling in the catalogue is manifest
-boilerplate churn, cross-cutting changes to the machinery applied N times — which is what an
-integration being wrong looks like from the outside.
-
-## Open questions
-
-Struck-through rows are answered, with where. The rest are live.
-
-### Settled
-
-| Question | Answer |
-|---|---|
-| ~~What does the graph **delete**?~~ | For the existing system: nothing, it is already there ([`analysis.md`](analysis.md)). For the design: the module/resource distinction, the interface as a kind of thing, capability checking as a separate mechanism, domain grouping, and — the first clear deletion — **grant kinds**, once a module may only be granted what it exclusively owns ([`worked-provider.md`](worked-provider.md)). |
-| ~~Is an interface a module, or a name?~~ | A **name**, and only where providers are genuinely substitutable. The adapter is what creates one; without an adapter there is a **tag**, which describes and does not bind ([`proposal.md`](proposal.md)). |
-| ~~Where do domain modules fit?~~ | They do not. There is core infrastructure — concrete modules named individually, not flavourable, nothing standing in front of them. |
-| ~~What happens to domain grouping?~~ | Superseded. Folders assert relationships; edges record them. What grouping was for is a tag and a query. [ADR 0009](../../02-DECISIONS/0009-modules-and-the-graph.md) is `proposed` and should be superseded rather than narrowed. |
-| ~~Is there one kind of edge?~~ | **No — two.** *Presence*, where a thing must exist, and *instantiation*, where a provider makes something for a consumer and hands back credentials. Instantiation implies presence, not the reverse. |
-| ~~When two modules provide one name, who chooses?~~ | Neither the consumer naming a node nor the consumer not caring. The consumer declares the **scope of its own need** — shared across its instances, or one each — the mesh binds, and the binding is written down and sticky. Where it is written follows the scope. |
-| ~~Can several modules share one database?~~ | **No.** A module is granted only what it exclusively owns — no shared writes and no read role on another's store, because reading couples you to its layout just as firmly. |
-| ~~What about a dashboard reading a dozen stores?~~ | **The rule is about contexts, not processes.** The mesh's own board reading the mesh's own store is the mesh showing its own data — not a boundary crossing. Everything inside a context reads its store freely; what is forbidden is a *different* context reading it. An earlier answer here was wrong. |
-| ~~Can every registry consumer be served another way?~~ | **Largely dissolves.** Of eighteen direct consumers, the owner keeps its database, node appliers are already stopped by ADR 0005, and the bulk are **foreign tenants** — thirteen tables across three contexts — who need to move out rather than read differently. |
-
-### Live
-
-| Question | Why it is open |
-|---|---|
-| How many instances should a module have? | Not derivable and not global: one-per-mesh is right for the broker and wrong for a store a disconnected node needs. It is per-module and nothing in the schema says it. |
-| Can two of something coexist? | `excludes` covers part of it. Two terminals are fine, two things wanting one port are not, two brokers might be either. |
-| What does a provider hand back? | Credentials and an address for a store; a command for a terminal. Same relation, different shape crossing it. |
-| Is provisioning one mechanism or two? | The mesh's own registry is provisioned **before there is a mesh**, so provisioning is part of the bootstrap and part of what the carried bundle expresses. At bootstrap the store is local; afterwards it is on another node. Same operation, both sides of a tier boundary. |
-| Is a tool surface one relation with two audiences, or two? | 56 of 126 modules carry tools — more than carry a service — and what consumes them is an **agent**, not a module. |
-| ~~Do the remaining cross-context reads want an interface or events?~~ | **Derived, not chosen.** Neither is SQL — that only ever runs against your own store. ADR 0004 makes disconnection ordinary, so anything that must work while disconnected cannot use a request and needs a local copy: a subscription. Anything where a stale answer is worse than none cannot use a subscription. |
-| What does a consumer do about events it missed while disconnected? | Replay from a point, ask once for a full picture and resume, or rebuild. The question every projection has, and unanswered here. |
-| What happens to a grant when its consumer is removed? | Dropping is data loss; keeping is a leak. ADR 0005's removal rule does not obviously carry, because the thing lives inside another module's state. |
-| Is a declaration composed per node, from what that node reported? | Some configuration follows the hardware. Either the host fills a blank — deciding, against ADR 0005 — or the control plane composes from the node's inventory first. |
-| Would `excludes` and capability requirements actually be used? | Zero manifests declare either, which is equally consistent with *nobody needs them* and *nobody can express them*. |
-| What does an exclusion mean for something already installed? | Refuse the install, or surface the conflict and let it be decided. |
-| Are tiers a view of the graph, or a constraint on it? | If a tier is a computed level the word is a convenience. If *a tier may depend only on tiers below it* is to be enforced, it is a constraint and must be stated as one. |
-| Where does resolution happen — the mesh, or the platform's package manager? | The mesh must model mesh-level edges. Whether it also resolves operating-system packages decides whether a solver gets written. |
-| How far may the control plane be split? | A single board over several contexts works because they are contexts *inside* one control plane with one interface. If a context becomes its own deployable with its own interface, the board is coupled to N of them and the composition has nowhere to live that tier 3 permits. A constraint on splitting, worth knowing before splitting. |
-| What does the pipeline schedule, once features are gone? | It schedules features today. The four categories they split into have different lifecycles, so the unit of work differs for each and needs naming. |
-| Should placement leave the catalogue? | A provision pins itself to a named node in the manifest. Placement is an inventory decision, and having it in the catalogue means a second node cannot provide the mesh's store without editing its consumer. |
diff --git a/01-RESEARCH/011-the-module-graph/analysis.md b/01-RESEARCH/011-the-module-graph/analysis.md
deleted file mode 100644
index ef423170..00000000
--- a/01-RESEARCH/011-the-module-graph/analysis.md
+++ /dev/null
@@ -1,138 +0,0 @@
-# The graph is not missing
-
-Measured against `origin/main` of the code repository, 2026-08-26. Every manifest, read through
-git refs rather than a checkout.
-
-The effort was opened to ask whether the catalogue's missing structure is a graph. It is not
-missing. **It exists, it is healthy, and three separate parts of the system already use it.**
-
-That is the finding, and it changes what is worth asking.
-
-## Finding 1 — the graph is already declared, and it is clean
-
-| | |
-|---|---|
-| manifests | 126 |
-| declare a dependency on another module | 64 |
-| declare a requirement on a provision | 11 |
-| **edges** | **103** |
-| declare neither | 62 — 49% |
-| **cycles** | **0** |
-| **dependencies declared but absent** | **0** |
-| deepest chain | 5 |
-
-Half the catalogue is unconnected, which matches
-[research 005](../005-domain-grouping/analysis.md)'s finding that fifty modules co-change with
-nothing. The connected half is well formed: no cycles, nothing dangling.
-
-## Finding 2 — it is already resolved, and already used
-
-The platform SDK carries a dependency resolver that topologically sorts modules, and it is
-called from three places: the tool loader at startup, the installer when syncing modules onto a
-node, and the delivery coordinator when expanding what a change affects.
-
-It also already does something the effort assumed would need designing: **a requirement on
-another module's provision is treated as an implicit edge to that module**, so a consumer does
-not have to declare the same relationship twice.
-
-So *ordering by the graph* — which
-[ADR 0005](../../02-DECISIONS/0005-the-node-host.md) says
-the control plane will do — is not a thing to build. It is a thing to call.
-
-## Finding 3 — the most important edges in the mesh are invisible
-
-The one place the graph is wrong, and it is wrong about the substrate.
-
-A module that needs a database declares it like this:
-
-```yaml
-provisions:
- - name: mesh-db
- provider: postgres # ← a dependency on the postgres module
- node: # ← and where it must run
-```
-
-`provider:` names a module. It is a dependency, declared, in the manifest — and it sits inside
-`provisions:`, which is what a module *offers*. The resolver reads `dependencies:` and
-`requires:`, so it never sees it.
-
-| | |
-|---|---|
-| provider references that name a real module | 4 |
-| **invisible to the resolver** | **3, across 2 modules** |
-
-Three edges is nothing, and they are the mesh's own database, the mesh's own broker, and the
-work engine's database. The most load-bearing relationships in the system are the ones the
-graph cannot see.
-
-**The consequence is measurable.** Computing what a working mesh needs, from the declared
-graph:
-
-```
- registry → sdk → mesh → meshware 4 modules, 4 levels
-```
-
-No database. No broker. A closure that is arithmetically correct and obviously wrong, and wrong
-for exactly one reason: a field that means *depends on* is not read as one.
-
-This is [04-ISSUES/003](../../04-ISSUES/003-firewall-scope-is-read-by-no-code/00-report.md)
-again, in a new place — not a key nothing reads, but a key read as something other than what it
-means.
-
-## Finding 4 — the resolver continues past faults it should stop on
-
-Two behaviours, both contrary to
-[ADR 0010](../../02-DECISIONS/0010-delivery.md):
-
-- **A cycle warns and falls back to input order.** A cycle means no correct order exists; the
- resolver proceeds with an arbitrary one and logs a line.
-- **A dependency that does not exist warns and continues.** The validation is documented as
- *non-fatal, logged as warnings*.
-
-Neither has fired in the current catalogue — there are no cycles and nothing dangling — which
-is why nobody has noticed. They are latent, and they are in the component that
-[ADR 0005](../../02-DECISIONS/0005-the-node-host.md) makes
-responsible for the ordering a host will apply without question.
-
-## Finding 5 — placement is decided in the catalogue
-
-`node:` in a provision pins it to a named node, in the manifest. Two modules do this today,
-and they are the substrate ones.
-
-Which node runs what is an inventory and placement decision — tier 2 by the skeleton's own
-test. Having it in a manifest means the catalogue decides placement, and a second node cannot
-provide the mesh's database without editing the module that consumes it.
-
-## What this means for the effort
-
-**The opening question — "what does the graph delete?" — has an answer: nothing, because the
-graph is already there.** The premise was wrong, and finding that out is the effort's first
-result rather than a setback.
-
-The questions that survive are narrower and answerable:
-
-| Missing declaration | Manifests using it today |
-|---|---|
-| `excludes` — installing A makes B unavailable | **0** |
-| a required node capability | **0** |
-| an interface, with adapters providing it | **0** |
-
-Those are what a graph would *add*. What it would delete is a different and smaller list, and
-the honest version of it is: nothing yet.
-
-**And two defects worth fixing regardless of what else this effort concludes:**
-
-1. `provider:` is a dependency edge and is not read as one. Fixing it makes the closure correct
- — which is what [research 012](../012-the-minimum-viable-node/00-overview.md) needs in order
- to answer what a one-node mesh requires.
-2. The resolver continues past a cycle and past a missing dependency. Both should refuse.
-
-## What was not measured
-
-- **Whether the missing declarations would be used.** Zero manifests declare exclusions or
- capabilities, and that is equally consistent with *nobody needs them* and *nobody can express
- them*. Nothing here separates those.
-- **Whether the interface-and-adapter idea has a consumer.** It is a good shape, and it is
- argued for rather than measured.
-- **What the closure should be.** Finding 3 says the computed one is wrong. It does not say
- what the right one is; that needs the fix first.
diff --git a/01-RESEARCH/011-the-module-graph/cases.md b/01-RESEARCH/011-the-module-graph/cases.md
deleted file mode 100644
index d4e7c730..00000000
--- a/01-RESEARCH/011-the-module-graph/cases.md
+++ /dev/null
@@ -1,128 +0,0 @@
-# What a module can be
-
-Every kind of thing the mesh has to install, run, own or know about, before deciding what a
-manifest says. Written to be argued with: a case here that turns out not to exist should be
-struck, and one that is missing is a hole in whatever schema follows.
-
-The hard cases are at the end, and they are the point.
-
-## The ordinary cases
-
-**1 — A supervised service.** A container the mesh runs and keeps running. Nobody starts it;
-it is simply up. *A relational store, a message broker, an object store, a dashboard.*
-
-**2 — A system package with configuration.** Not a container. Installed into the machine,
-configured through files, run by the service manager. *A firewall, a resolver, an overlay.*
-Note: [ADR 0005](../../02-DECISIONS/0005-the-node-host.md) says applying
-these is the host's job — so what the module contributes is the *deciding*, not the doing.
-
-**3 — An application a person launches.** Installed on a node, started by a human, running only
-while they use it. *An editor, a chat client, a file manager, a terminal.*
-
-**4 — A command-line tool.** Installed, on the path, run when invoked. No service, no window.
-*A formatter, a query client, a backup utility.*
-
-**5 — A library.** Never runs at all. Consumed at build time by other modules. *An SDK.*
-
-**6 — A one-shot task.** Runs once, changes something, exits. *A schema migration, a data
-import, a seed.*
-
-**7 — A scheduled task.** Runs repeatedly on a timer, exits each time. *A backup, a prune, a
-report.*
-
-**8 — An adapter.** Exists to make several unlike things look alike behind one name. *A model
-provider behind an assistant interface.*
-
-**9 — A standalone application in its own repository.** Same shape as any of the above; the
-difference is only where its source lives
-([ADR 0015](../../02-DECISIONS/0015-applications-live-in-their-own-repository.md)). Worth
-listing because a schema that assumes a monorepo path would exclude it.
-
-## The cases that break a naive schema
-
-**10 — Something that is a service *and* an application.** A git forge is consumed by other
-modules as a remote and a registry, *and* operated by a person through a web interface. A web
-analytics service grants a tracking identity *and* is a dashboard somebody reads. Neither is a
-service-or-application choice; both are true simultaneously.
-
-**11 — Something that provides to others *and* consumes from others.** The store provides
-databases and needs a filesystem. The forge provides a registry and needs a database. Provider
-and consumer are not kinds of module; they are ends of edges.
-
-**12 — Something the mesh installs that then becomes a node capability.** The container runtime
-is installed *by* the mesh, and once it works, the node **provides** `container-runtime` to
-everything else. So a module can change what its node provides. The node's provides-list is
-therefore partly derived from what is installed on it, not only detected from what was already
-there — and the two have to agree.
-
-**13 — Something that must be adopted rather than installed.** The machine already has the
-package manager, the container runtime, possibly the version control system, each with
-configuration somebody chose. The module does not install it; it takes it over
-([research 012](../012-the-minimum-viable-node/00-overview.md)).
-
-**14 — Something that is a set, not a thing.** *Core infrastructure* is not installable — it is
-a name for the concrete modules a working node needs. Whether that is a module whose only
-content is `requires`, or a query over the graph, or a pinned list outside the catalogue, is
-undecided and is one of the sharper questions here.
-
-**15 — Something with one instance for the whole mesh.** There is one mesh database, not one per
-node. Assigning it to two nodes is not redundancy, it is two meshes. Contrast with a terminal,
-where per-node is the only sensible reading.
-
-**16 — Something that may be installed several times over.** Two terminals coexist happily. Two
-things wanting port 443 do not. Two message brokers might be fine or might be a split brain,
-and nothing in *provides* and *requires* distinguishes those.
-
-**17 — Something that is not software at all.** A firewall policy. A DNS record. A certificate.
-It owns no binary, runs nothing, and is entirely *desired state* — which is the one case that
-fits the host's declaration model exactly and fits an installable-package model not at all.
-
-**18 — An agent.** The mesh's own premise is that agents are participants. An agent has an
-identity, a licence, a node it runs on, and work it does. Whether that is a module, a record in
-the control plane, or something else is not obvious, and getting it wrong shapes everything
-about how agents are assigned.
-
-**19 — The host itself.** Tier 0 installs everything else and is installed by hand. It is not a
-module, and a schema that cannot say so has a bootstrap problem hiding in it.
-
-**20 — Something the mesh depends on and does not control.** A domain registrar, an upstream
-resolver, a certificate authority, an electricity supply. Almost certainly not modules — but
-the mesh's health depends on them, and they are the reason a node can be perfectly configured
-and still not work.
-
-## What varies across the cases
-
-The list above matters less than this. These are the axes a manifest has to express, and each
-one is a question the schema must answer or deliberately refuse.
-
-| Axis | Range | Sharpest case |
-|---|---|---|
-| **Does it run?** | supervised · launched by a person · once · on a timer · never | 5, 6, 7, 17 |
-| **Who starts it?** | the mesh · a human · nothing | 1 vs 3 |
-| **Where does it come from?** | container image · system package · our source · already on the machine | 12, 13 |
-| **Does it provide to other modules?** | a resource · an abstract name · nothing | 8, 11 |
-| **Does it hold state?** | yes, and it matters where · no | 1 vs 3 |
-| **How many instances?** | one per mesh · one per node · many per node | 15, 16 |
-| **Can two coexist?** | yes · no · only with different settings | 16 |
-| **Is it installed or adopted?** | installed · adopted · either, depending on the machine | 13 |
-| **Does installing it change what the node provides?** | yes · no | 12 |
-
-**Two of these are not in any current thinking**, and both come from the hard cases:
-*how many instances* (15) and *can two coexist* (16). `excludes` covers part of the second and
-nothing covers the first.
-
-## Questions the cases raise
-
-- **Is "runs" a property or a kind?** The axes suggest a property — one schema, with a field
- saying how it runs, `never` included. The alternative is several kinds of module with
- different schemas, which is the taxonomy [research 011](00-overview.md) already rejected once
- for services and applications.
-- **Is an agent a module?** (18) If yes, the schema carries identity and licensing. If no, the
- mesh has two catalogues.
-- **Is core infrastructure a module?** (14) A module whose only content is `requires` is either
- elegant or a category pretending to be a thing — the same trap `database` was.
-- **What names one-per-mesh?** (15) Nothing in provides, requires or excludes says it, and
- getting it wrong means two of something that must be one.
-- **Where does a policy live?** (17) Pure desired state fits the host's declaration exactly.
- Whether it is a module at all, or something the control plane derives and no catalogue entry
- exists for, is open.
diff --git a/01-RESEARCH/011-the-module-graph/features.md b/01-RESEARCH/011-the-module-graph/features.md
deleted file mode 100644
index 84c1216c..00000000
--- a/01-RESEARCH/011-the-module-graph/features.md
+++ /dev/null
@@ -1,131 +0,0 @@
-# What a feature is, and what it splits into
-
-Measured against `origin/main` of the code repository, 2026-08-26.
-
-The operator wants the feature concept gone.
-[Research 006](../006-mesh-from-scratch/00-overview.md) left it open — *"does `feature`
-survive? The skeleton splits it in two and argues the conflation is what makes the delivery
-pipeline hard to reason about."*
-
-This is what it actually is, and what it turns into.
-
-## What it is today
-
-A **feature** is a kind of content a module can carry, detected from what its directory
-contains rather than declared. Twenty-one of them, each with a handler that owns its whole
-lifecycle:
-
-```
-configs dist-assets events hooks migrations migration-artifact npm npm-install
-prerequisite-env prerequisite-packages prerequisite-provision provision-migrations
-provision-seeds seeds service systemd tools verifiers verify vhost detect
-```
-
-Each moves through **six stages**: build, publish, install, configure, start, verify.
-
-## Finding — every handler implements every stage
-
-The structural evidence, and it is not a style problem.
-
-| handler | stages it implements |
-|---|---|
-| `configs` | build · configure · install · start · verify |
-| `service` | build · configure · install · start · verify |
-| `systemd` | build · configure · install · start · verify |
-| `tools` | build · configure · install · start · verify |
-| `vhost` | build · configure · install · start · verify |
-| `migrations` | build · configure · install · start · verify |
-| `npm` | build · publish · install · configure · start |
-
-`configs` writes files onto a node. It has nothing to build, and it has a build stage.
-`npm` publishes a package to a registry. It has nothing to start, and it has a start stage.
-
-**One interface spans build-time and apply-time, so every kind of content must implement both
-halves and most of them do nothing in one.** That is the conflation, and the emptiness is what
-makes the pipeline hard to reason about: a stage that does nothing and a stage that failed to
-do anything look identical from outside.
-
-## What it splits into
-
-The twenty-one are not one kind of thing. They are four, and they belong to different tiers.
-
-**Artifacts — built once per version, then published.** `npm`, `dist-assets`,
-`migration-artifact`. Nothing about a node is involved; the output is a thing that exists in a
-registry. **Tier 2, delivery.**
-
-**Resources — desired state on a machine.** `configs`, `service`, `systemd`, `vhost`, `tools`.
-Applied, converged, idempotent — which is exactly what
-[ADR 0005](../../02-DECISIONS/0005-the-node-host.md)
-already describes and what the host already does. **Tier 0.**
-
-**Actions — run once, against something that is not this machine.** `migrations`, `seeds`,
-`provision-migrations`, `provision-seeds`, `hooks`. A migration runs against a database, and the
-database may be on another node entirely. Neither an artifact nor node state, which is why they
-sit awkwardly in a scheme built for both. **Tier 2, and the operator wants seeds gone.**
-
-**Checks — assertions, not changes.** `prerequisite-env`, `prerequisite-packages`,
-`prerequisite-provision`, `verify`, `verifiers`, `detect`. The prerequisites are **requirements
-in disguise** — a module saying what must be true before it can be installed, which is precisely
-what an edge in the graph says. The verifiers are the read-back the host already performs.
-
-## So the answer is: it splits, and the split is a tier boundary
-
-**`feature` is one word for four things spanning three tiers.** That is why every handler
-implements every stage, why half of them are empty, and why the pipeline is hard to reason
-about.
-
-Nothing replaces it, because it was never one concept:
-
-| Was a feature | Becomes | Whose |
-|---|---|---|
-| npm, dist-assets, migration-artifact | an **artifact** | delivery |
-| configs, service, systemd, vhost, tools | a **resource** in a declaration | the host |
-| migrations, hooks | an **action** against something else | delivery |
-| seeds | *nothing* — the operator wants them gone | — |
-| prerequisite-* | an **edge** in the graph | the catalogue |
-| verify, verifiers | the host's **read-back** | the host |
-
-## Tools are a facet, and their consumer is not a module
-
-The most common content in the catalogue: 56 of 126 modules carry a tool surface, more than
-carry a service.
-
-It survives the split, and it does not fit either half cleanly. A tool is not an artifact and
-not node state — it is a **contract the mesh publishes on a module's behalf**, and what consumes
-it is an **agent**, not another module.
-
-That is a second audience, and the design has only described one. A module `provides` things
-other modules require; a module also `provides` things agents call. Same word, different
-consumer, different lifecycle — a tool appears when the module is assigned somewhere and
-disappears when it is not, and nothing in the graph edges says so.
-
-Whether that is one relation with two audiences or two relations is undecided, and it is the
-kind of question that is cheap now and expensive later.
-
-## What is lost, and should not be
-
-**Detection.** Features are detected from the directory rather than declared, and the reason is
-good: *a declared list and the directory it describes drift, and the directory is the one that
-is true.* That property is worth keeping whatever the concept is called — a module that says it
-has migrations and has none, or has them and does not say so, is a fault nobody sees until it
-matters.
-
-Under the split, detection still applies: what a module **contains** is read from what is there.
-What it **requires**, **provides** and **excludes** is declared, because none of that is visible
-in a directory.
-
-That line is worth stating precisely, because it is the one the current design got right:
-**content is detected, relationships are declared.**
-
-## Open
-
-- **Is an action a resource?** A migration is not node state and not an artifact. It could be a
- resource type the host applies with the target being a database rather than a machine — which
- would collapse the third category into the second, at the cost of the host reaching something
- that is not the machine it is on. That cost looks too high, but it has not been argued.
-- **Where do hooks go?** They are arbitrary code a module runs at a stage — the escape hatch. A
- design with no escape hatch is either very good or has not met reality yet, and this one has
- not.
-- **What does the pipeline schedule, once features are gone?** It currently schedules features.
- If the four categories have different lifecycles, the unit of work is different for each, and
- what the coordinator orchestrates needs naming.
diff --git a/01-RESEARCH/011-the-module-graph/proposal.md b/01-RESEARCH/011-the-module-graph/proposal.md
deleted file mode 100644
index 21e52972..00000000
--- a/01-RESEARCH/011-the-module-graph/proposal.md
+++ /dev/null
@@ -1,167 +0,0 @@
-# One kind of edge
-
-> **Superseded in part by [`worked-provider.md`](worked-provider.md).** Working postgres through
-> completely shows there are **two** kinds of edge, not one: *presence* — the thing exists and is
-> reachable, nothing created — and *instantiation* — the provider is asked to make something for
-> this consumer and hands back credentials. Instantiation implies presence; presence does not
-> imply instantiation. Everything else below stands; the claim in the title does not.
-
-A design, not an account of what exists. [`analysis.md`](analysis.md) measured the current
-catalogue and its value here is two lessons rather than its machinery: a field that means
-*depends on* should say so, and placement does not belong in a manifest.
-
-## The idea
-
-**A module provides names. A module requires names. That is the only edge.**
-
-Everything the effort listed as separate declarations turns out to be one relation with
-different kinds of name on either end.
-
-```
-postgres provides postgres
-kitty provides kitty, terminal
-xterm provides xterm, terminal
-anthropic provides anthropic, ai-assistant
-
-meshboard requires postgres, lavinmq
-vscode requires terminal, display-server
-```
-
-A name is either **concrete** — a module's own name, so `requires: postgres` means that module
-and no other — or **abstract**, so `requires: terminal` means whatever provides it.
-
-That single relation absorbs three things the effort had listed separately:
-
-| Was going to be | Is |
-|---|---|
-| requires another module | requires a concrete name |
-| requires a resource | requires the module that provides it, by name |
-| an interface, with adapters providing it | an abstract name, legitimate only where an adapter makes providers substitutable |
-
-The interface stops being a kind of module. It is a name with more than one provider *and a
-contract they all satisfy*, and nothing has to declare that it is one.
-
-### An abstract name is only legitimate when providers are actually substitutable
-
-The test, and it is a strict one: **can a consumer be switched from one provider to another
-without changing?**
-
-`terminal` passes. Anything that runs a command in a terminal works, and a consumer never
-learns which one it got.
-
-**`database` fails, and it is the example worth keeping.** A module connecting to Postgres does
-not connect to MongoDB, or to SQL Server, or to MariaDB. Different wire protocol, different
-dialect, different driver. A consumer that declared `requires: database` and was handed any of
-them would break — so the name promises something no provider can deliver, and the resolver
-would satisfy a requirement that is not satisfied.
-
-That failure has a shape this repository already knows: something declared, accepted, and not
-true. It is worse here than usual because the graph would report success.
-
-**The adapter is what creates an interface.** `ai-assistant` is a legitimate abstract name
-exactly when adapters exist to normalise the providers behind it. Without an adapter there is
-no interface — there is a category, and a category is not an edge.
-
-So `postgres` is required by name, and a module that could genuinely work with several stores
-requires whichever it actually speaks to.
-
-### Categories are catalogue metadata, not structure
-
-*Database* is still a useful word — for finding things, for a person browsing what the mesh can
-host, for grouping in an interface. It is a **tag**.
-
-Tags describe. Edges bind. Keeping them apart is what stops the catalogue acquiring a second
-kind of relationship that looks like a dependency and is not — which is what a folder named
-after a domain already was.
-
-## The node provides names too
-
-The move that makes capabilities stop being a separate system.
-
-A node's profile is a set of provided names. `display-server`. `container-runtime`.
-`amd64`. A module requiring `display-server` is satisfied by **the node**, exactly as a module
-requiring `postgres` is satisfied by another module.
-
-So there is one resolution rather than two: *is this name provided by anything available here?*
-A graphical application cannot be installed on a node with no display server for the same
-reason, and through the same code, that it cannot be installed without its libraries.
-
-And it means the host's capability detection — which already reports what a machine can be
-asked to do, with a reason for each verdict — **is the node's provides-list**. It was built to
-be read by a person; it turns out to be an input to resolution.
-
-## What a module declares
-
-```yaml
-name: meshboard
-
-provides: [meshboard]
-requires: [postgres, lavinmq, container-runtime] # by name: it speaks their protocols
-excludes: []
-tags: [observability] # for finding it, never for resolving it
-
-runs: # what applying it means
- - container: ...
-constraints: # what must be true of a node, never which node
- - architecture: amd64
-```
-
-`postgres` and `lavinmq` are concrete because this module speaks their wire protocols and would
-break against anything else. `container-runtime` is abstract and satisfied by the **node**.
-
-**`excludes`** is the one genuinely new relation: naming something that cannot coexist with
-this. It is not derivable from requires and provides, and without it two modules that both
-provide `terminal` — or two that both want port 443 — look independent right up until
-installing the second breaks the first.
-
-**`constraints` are not placement.** They say what must be true of a node, never which node.
-Which node runs what is an inventory decision, and the measurement found the current catalogue
-deciding it in the manifest — a module pinning its database to a named node, so that a second
-node cannot provide it without editing the module that consumes it. That is the mistake this
-separation exists to avoid.
-
-## What it deletes
-
-The question the effort opened with, answered for the design rather than for what exists.
-
-- **The distinction between requiring a module and requiring a resource.** One relation.
-- **The interface as a kind of thing.** A name with several providers.
-- **Capability checking as a separate mechanism.** The node is a provider.
-- **The domain module** — a `networking` module that exists to gather a firewall, a resolver and
- a proxy under one name. It came from an older shape and does not fit: there is no such thing
- to install. There is **core infrastructure**, which is a set of concrete modules named
- individually — a firewall, a store, a resolver — with no flavour and no grouping module
- standing in front of them.
-- **Domain grouping as structure** ([ADR 0009](../../02-DECISIONS/0009-modules-and-the-graph.md)).
- Folders assert relationships; edges record them. What grouping was for — finding things,
- seeing what belongs together — is a **tag** and a *query* over the graph, neither of which
- anybody has to keep true by hand.
-- **Tiers as a separate concept**, possibly. A tier is a level in the graph, and levels are
- computed. Whether the coarse boundary is still worth naming is below.
-
-## What it does not delete, and should not
-
-**A resolver still has to exist**, and it will need version constraints, conflict handling and
-a story for when two modules provide one name and nothing says which to use. That is the
-long-solved and easily-botched part, and the design should say what it **delegates** to the
-platform's package manager rather than reimplementing.
-
-## Open
-
-- **When two modules provide one abstract name, who chooses?** A node with both `kitty` and
- `xterm` satisfies `terminal` twice. Whether the choice is a node setting, a mesh setting, or
- an explicit pin is the substance of the interface idea, and this proposal does not settle it.
- It is a smaller question than it was: the strict substitutability test means the consumer
- genuinely does not care which it gets, so the choice is about preference rather than
- correctness.
-- **Are tiers a view of the graph, or a boundary that survives it?** If tiers are levels, the
- concept is derived and the word is a convenience. If the tier rule — *a tier may depend only
- on tiers below it* — is meant to be enforceable, it is a constraint on the graph rather than
- a description of it, and it has to be stated as one.
-- **What does a provider hand back?** A consumer requiring `postgres` needs credentials and an
- address; one requiring `terminal` needs a command. The requirement is satisfied by the same
- relation in both cases, and what flows across it is not the same shape. Whether that lives in
- the name, beside it, or in what the provider returns is undecided.
-- **What does a node provide that is not a capability?** Its architecture is a provided name
- under this scheme, and so is its operating system. That may be elegant or may be one
- abstraction too far; nothing here tests it.
diff --git a/01-RESEARCH/011-the-module-graph/worked-provider.md b/01-RESEARCH/011-the-module-graph/worked-provider.md
deleted file mode 100644
index a92fc081..00000000
--- a/01-RESEARCH/011-the-module-graph/worked-provider.md
+++ /dev/null
@@ -1,491 +0,0 @@
-# A provider, all the way through
-
-One module, worked out completely, because it is the case that breaks the tidy version. It
-looks like *a container that runs a database* and it is at least nine things.
-
-Postgres is the example. **The shape is not specific to it** — see the end.
-
-## What it carries
-
-**1 — A supervised container.** An image, a version, a data volume, and configuration. Easy,
-and the only part the phrase "a docker service" describes.
-
-**2 — Persistent state, and where it lives matters.** The volume is the database. Moving this
-module between nodes is not rescheduling; it is a migration. Almost nothing else in the
-catalogue has this property, and nothing in `provides` / `requires` expresses it.
-
-**3 — Configuration that is partly the machine's.** Tuning follows the hardware — memory,
-storage. A declaration generated centrally cannot know those, and
-[research 012](../012-the-minimum-viable-node/00-overview.md) says the machine's own values win
-on conflict. So some of this module's configuration is *derived from the node it lands on*.
-
-**4 — A tool surface.** It exposes capabilities agents can call — query, list, provision. That
-is not a resource on a machine and not an artifact; it is a contract the mesh publishes on the
-module's behalf.
-
-**5 — A provisioner.** The part that matters, and the one below.
-
-**6 — Its own bookkeeping.** The provisioner must remember what it granted to whom, or it
-cannot revoke, rotate or clean up. So a module that provides state to others *also* holds state
-about its providing — and that state is not the database's data.
-
-**7 — An exposure decision, per node it runs on.** Reachable from the machine only, from the
-local network, or publicly. That is a property of *this assignment*, not of the module: the
-same module on two nodes may answer differently.
-
-**8 — Credentials it generates.** Per consumer, and they have to reach the consumer. Which
-means a provisioning edge carries a payload, and the payload is a secret.
-
-**9 — Health that is not "the container is up".** A container running and a database accepting
-connections are different facts, and the second is the one anything cares about. This is the
-host's read-back rule, at a distance.
-
-## The provisioner is a second kind of edge
-
-The tidy version of this effort said: *a module provides names, a module requires names, that is
-the only edge.* A small game wanting to store data shows it is not.
-
-```
-my-cool-game requires postgres # I speak its protocol, it must exist
-my-cool-game requires a database FROM postgres, called my-cool-game
-```
-
-The first is **presence**: the thing exists and is reachable. Nothing is created; nothing flows
-back. `vscode requires terminal` is this, and so is `requires container-runtime`.
-
-The second is **instantiation**: the provider is asked to make something *for this consumer*,
-and hands back what the consumer needs to use it. A database, a user, a password, an address.
-
-They differ in every way that matters:
-
-| | presence | instantiation |
-|---|---|---|
-| creates something | no | yes, one per consumer |
-| carries a payload back | no | credentials, an address |
-| can be revoked | — | yes, and must be when the consumer goes |
-| provider holds state about it | no | yes — who was granted what |
-| satisfied by | anything providing the name | that provider, specifically |
-
-**This is the mesh's actual power**, in the operator's words: a small game declares it wants a
-database and the mesh makes one. Nobody creates a user by hand, nobody pastes a connection
-string. That is worth being precise about rather than folding into a single relation because
-one relation is prettier.
-
-## What that costs the design
-
-**The proposal's "one kind of edge" is wrong**, and the current system already knew: it has
-`dependencies` for presence and `requires: provision:` for instantiation, with the resolver
-deriving a presence edge from every instantiation edge. [`analysis.md`](analysis.md) recorded
-that derivation as a convenience. It is not — it is the correct relationship between two
-genuinely different relations.
-
-So: **two kinds of edge, one graph.** Instantiation implies presence. Presence does not imply
-instantiation.
-
-## Which provider, when two nodes run one
-
-Asked directly, because two nodes can each run a relational store and a consumer has to be
-served by one of them. Neither obvious answer is right.
-
-**Not "the consumer names the node."** That is placement in the consumer's manifest — a small
-game edited because a database moved, which is the fault
-[`proposal.md`](proposal.md) separates constraints from placement to avoid.
-
-**Not "the consumer does not care" either.** For presence it genuinely does not: a terminal is a
-terminal. For instantiation it cares permanently, because the data lands in exactly one store
-and the wrong choice is discovered long afterwards.
-
-**What the consumer does know is the scope of its own need.** Not which node — how many of the
-thing it wants, relative to itself:
-
-| Scope | Means | Example |
-|---|---|---|
-| **shared** | one instance serves every instance of this consumer | the mesh's own registry: every node reads the same rows |
-| **per instance** | each instance of this consumer gets its own | a local cache, a per-node queue |
-
-That is a property of the consumer, expressible without naming anything. And it is the thing
-that actually decides: a shared need cannot be satisfied by a provider each node runs
-separately, and a per-instance need should not be satisfied by a shared one.
-
-**Then the mesh binds, and the binding is written down.** Not recomputed: a resolver that
-re-derives which store serves a consumer will one day derive a different answer and relocate a
-database, so the binding is made once and changed only deliberately.
-
-**Where it is written down follows the scope.** A shared grant belongs to the *module* and every
-assignment of it references the same one — which is the answer for one module installed on two
-nodes wanting one database between them. A per-instance grant belongs to the *assignment*. Same
-relation, two homes, and which home is not a detail: it is what makes two instances share
-something or not.
-
-The pieces that follow, none of them settled here:
-
-- **When several providers satisfy the scope**, something chooses — most plausibly locality,
- preferring a provider on the same node. That is a default, and it must be overridable, because
- the reason to override it is exactly the reason nobody anticipated it.
-- **A binding is a thing that can be wrong.** Once recorded it can be inspected, and a consumer
- bound to a store on a node that no longer exists is a question somebody can be asked rather
- than a failure at connect time.
-- **Moving a binding moves data.** Whatever the mechanism, changing it is a migration and not a
- configuration change, and a design that lets it look like the latter will lose something.
-
-## Provisioning is early, not late
-
-An assumption worth killing: that provisioning is something the control plane does for
-consumers once a mesh is running.
-
-**The mesh's own registry is a provisioned database.** So is its virtual host on the broker.
-Neither exists until something creates them, and nothing in the mesh works until they do. The
-order is:
-
-```
-1 the store runs from the bundle the host carries
-2 a database is created in it a provisioning step
-3 the mesh's own schema is applied a migration, against that database
-4 the control plane starts and only now is there a mesh
-5 everything else is provisioned the ordinary path
-```
-
-Steps 2 and 3 happen **before there is a mesh to do them**. So provisioning is not a
-control-plane service that consumers use; it is part of the bootstrap, and part of what the
-carried bundle has to be able to express.
-
-**Which strains what a declaration is.** [ADR 0005](../../02-DECISIONS/0005-the-node-host.md)
-has the host applying *declared state on this machine*. A database inside a running store is not
-a file or a unit — and at bootstrap it is, at least, local: the store is on the same machine as
-the host applying the bundle.
-
-Later it is not. A consumer on one node provisioned from a store on another is the ordinary
-case, and reaching it is not the host's job.
-
-**Resolved as two mechanisms, which is the answer rather than a compromise**
-([ADR 0005](../../02-DECISIONS/0005-the-node-host.md)). The host
-runs bootstrap actions locally from the bundle; the control plane provisions across the mesh
-afterwards. Different actors, different scopes, different trust paths — so there is no single
-operation with a tier boundary running through it.
-
-## Several modules, one database — and the case for refusing
-
-Asked, then reconsidered by the operator: *maybe we should not allow it.*
-
-The permissive version was a per-consumer **schema** inside a shared database — its own
-namespace, its own migrations, revocable by dropping the schema, with a cross-context join
-possible but deliberate.
-
-**The stricter version is better, and it goes further than schemas.**
-
-> **A module is only ever granted a resource it exclusively owns.**
-
-No shared writes. And **no read-only role on another module's database either** — reading
-another context's tables couples you to its layout exactly as firmly as writing them does, and
-the coupling is harder to see because nothing breaks until the owner changes a column.
-
-That is what [`how-we-build`](../../00-META/how-we-build.md) §4 already says: *contexts integrate
-through the record, never through a shared schema.* The permissive version kept the letter of it
-and left the temptation in place, and the path of least resistance wins eventually. A boundary
-that is merely inconvenient to cross is a boundary that gets crossed.
-
-### What it costs
-
-**Cross-module reporting.** Anything wanting to know what several modules hold can no longer
-join across them. It consumes their events, or calls their interface, and neither is as
-immediate as a query.
-
-That cost is the point rather than a regrettable side effect — it is §4's whole argument, and
-the mesh already has both mechanisms: an event stream every context publishes to, and a tool
-surface every module exposes. What gets harder is the thing that was making work belonging to
-one context keep having to be implemented in another.
-
-**One more connection per consumer.** A dozen modules means a dozen databases rather than a
-dozen schemas in one. For a relational store this is unremarkable; it is worth stating only so
-nobody discovers it as a surprise.
-
-### What it deletes
-
-The effort has been looking for what the design *removes* rather than adds, and this is the
-first clear instance:
-
-- **Grant kinds.** There is one — an exclusive resource. No schema grants, no read roles, no
- scoping rules for who may see what inside a shared thing.
-- **The question of who owns which table**, and with it the guessing at revocation time.
-- **Cross-module migration ordering.** Two modules migrating one database need their migrations
- ordered against each other. Exclusive ownership means a module's migrations are ordered only
- against itself.
-- **A whole class of permission modelling** that a shared store would otherwise need.
-
-### The rule is about contexts, not processes
-
-An earlier version of this file argued that a dashboard reading a dozen stores was caught by the
-rule, because a dashboard is a surface and surfaces speak to an interface. **That was wrong, and
-it drew the line in the wrong place.**
-
-The mesh's own board showing nodes, modules and deployments is not a separate context reaching
-across a boundary. It is the mesh showing its own data. Reading that store is not a violation
-however it is done, and requiring it to go through an interface to reach facts its own context
-owns would be ceremony.
-
-**The rule is: a context is granted what it exclusively owns.** Everything inside that context —
-its service, its surface, its tools — reads it freely. What is forbidden is a *different*
-context reading it.
-
-Which is exactly what the count below shows: the problem was never surfaces. It was three other
-contexts keeping their tables in the mesh's database.
-
-**And one surface over several contexts is normal.** The board visualises the mesh, the work
-engine, the knowledge base and more, and the alternative — a separate web application per
-context — is worse for everyone who uses it. That is not a compromise with the rule; composing
-several sources into one view is what a surface *is*.
-
-What it changes is only where it reads from: each context's **interface**, not each context's
-**store**. Most of that already exists — 56 of 126 modules carry a tool surface, more than carry
-a service.
-
-**And the unified board is what keeps those interfaces honest.** If a view cannot be built from
-a context's interface, that interface is inadequate — discovered in the one place where it is
-cheap to notice, rather than the first time something else needs the same data and quietly
-reaches for the store instead.
-
-**But "the board calls each context's interface" is not quite it either**, and the objection is
-right: if every context runs its own service with its own interface, the board is coupled to N
-of them instead of N schemas, something has to compose them, and composition is logic — which
-tier 3 says a surface does not hold. That moves the problem up a layer rather than solving it.
-
-**The skeleton already answers this and the argument above talked past it.** `work` and
-`knowledge` are not separate services; they are **contexts inside the control plane**, alongside
-the record, inventory, delivery and the rest — and `api` is listed there as *the one interface
-every surface speaks to*.
-
-So the board speaks to **one** interface. Behind it the contexts stay separate: separate stores,
-integrating through the record. But they are one tier, one repository, one deployable, and
-coupling *within* a tier is not what the tier rule forbids.
-
-Which resolves the objection rather than deflecting it: the problem does move up a layer, and
-the layer it moves to already exists and has this as its job.
-
-**The caveat is load-bearing.** This holds only while the contexts are not separate deployables.
-The moment one becomes its own service with its own interface, the board is back to N clients,
-something must compose them, and the composition has nowhere to live that tier 3 permits. That
-is a real constraint on how far the control plane may be split, and it is worth knowing now
-rather than discovering it by splitting.
-
-If composing even one interface turns out too slow, the answer is a projection the board owns
-and keeps current from events — not access to somebody else's tables.
-
-### Checked against the real consumers
-
-The rule's survival turned on whether every reader of the mesh's own registry could be served
-some other way. **Eighteen consumers open a direct connection to it.** Four groups, and only one
-of them is work.
-
-| Group | What happens under the rule |
-|---|---|
-| **The owner and its machinery** — the mesh module, the SDK, the environment and configuration synchronisers, secrets | Nothing. It owns the database. |
-| **Node appliers** — the overlay, the shell daemon, the resolver | **Already resolved.** [ADR 0005](../../02-DECISIONS/0005-the-node-host.md) stops the host querying the mesh database, decided for tier reasons with nothing to do with this. |
-| **Foreign tenants** — the work engine (10 tables), the knowledge base (2), pipeline logs (1) | They need **their own database**. They are not reading the registry; they are storing their own data in it. |
-| **Genuine cross-context reads** — the work engine reads `nodes`; two others read a handful | The only ones needing an interface or events. |
-
-**Thirteen foreign tables live in the mesh's registry database**, belonging to three separate
-contexts. That is [`how-we-build`](../../00-META/how-we-build.md) §4's shared schema, counted.
-
-**So the question was the wrong shape.** The bulk of the problem is not readers needing a new
-route to data — it is **tenants needing to move out**. Tasks, agents and teams have nothing to do
-with nodes and modules; they are co-located by history. Give that context its own database and
-its dependency on the registry shrinks to a single table.
-
-What remains is a handful of genuine cross-context reads, small enough to enumerate rather than
-estimate. **The rule holds.**
-
-### Request or subscription, and what decides
-
-The remaining cross-context reads need one or the other. **Neither is SQL** — under exclusive
-ownership a module runs SQL against its own database and nothing else, whatever transport a
-query might travel over. Both options are the mesh's own channel, and both ride the broker
-([ADR 0002](../../02-DECISIONS/0002-nodes-communicate-over-a-broker.md)), so the transport is
-not the distinction.
-
-**The distinction is where the answer lives when you need it.**
-
-| | request | subscription |
-|---|---|---|
-| you ask | at the moment you need to know | never — you are told |
-| the answer lives | on the other side | in your own store |
-| freshness | always current | as current as the last event you received |
-| when the other side is down | you cannot answer | you answer from your copy |
-| what you must handle | a round trip that can fail | events you missed while you were down |
-
-**What decides is not taste.** [ADR 0004](../../02-DECISIONS/0004-a-node-and-how-it-joins.md)
-makes disconnection an ordinary situation rather than an exception. So:
-
-> **Anything that must keep working while disconnected cannot use a request** — there is nobody
-> to ask. It needs a local copy, which means a subscription.
-
-And the converse: anything that must be *correct at the instant of asking*, where a stale answer
-is worse than no answer, cannot use a subscription. A display can lag. A decision about whether
-a grant is still valid cannot.
-
-That turns an apparently open question into a derived one. What remains genuinely open is
-narrower: **what a consumer does about the events it missed** while it was disconnected — replay
-from a point, ask once for a full picture and resume, or rebuild from scratch. That is the same
-question every projection has, and nothing in the record answers it yet.
-
-## A migration belongs to the consumer and runs on the provider
-
-A game defines migrations. They run against the database the store granted **it**. So the
-migration is:
-
-- **owned** by the consumer — it is that module's schema, versioned with that module;
-- **hosted** by the provider — it runs inside something the consumer does not control;
-- **ordered** after the provisioning edge — there is nothing to migrate until the grant exists;
-- **scoped** to the grant — the consumer's migrations touch its database and no other.
-
-Ownership crosses the edge, which nothing in *provides* and *requires* expresses. And it is the
-*action* category from [`features.md`](features.md) made concrete: not an artifact, not node
-state, and not something the host can apply, because the thing it changes is not the machine.
-
-It also gives a consumer's own install an internal order — **provisioned, then migrated, then
-started** — that depends on an edge rather than on the module's contents.
-
-## What still has no answer
-
-**How many instances of postgres should exist?** One per mesh is wrong — a node that must work
-while disconnected cannot depend on a database elsewhere. One per node is wrong — the mesh's own
-registry is one thing, not one per node. So the answer is per-module, and nothing in the schema
-says it. This is [`cases.md`](cases.md) axis *how many instances*, and postgres is the case that
-proves it cannot be a global rule.
-
-**What happens to a grant when the consumer is removed?** The game is uninstalled. Its database
-still exists, holding its data. Dropping it silently is data loss; keeping it forever is a leak.
-[ADR 0005](../../02-DECISIONS/0005-the-node-host.md) says
-the host removes what it applied and no longer declares — but this is not on the host, it is
-inside another module's state, and the same reasoning does not obviously carry.
-
-**Where does node-derived configuration come from?** (3) The control plane composes a
-declaration, and cannot know this machine's memory. Either the host fills in a blank the
-declaration leaves — which makes the host decide something, against
-[ADR 0005](../../02-DECISIONS/0005-the-node-host.md) — or the control
-plane reads the node's inventory first and composes with it. The second is consistent and means
-a declaration is composed *per node from what the node reported*, which is a stronger claim than
-anything recorded so far.
-
-
-## Providing is not a substrate thing
-
-The four pinned services are the obvious providers, and they are not a category.
-
-| Service | What a consumer asks it for |
-|---|---|
-| a relational store | a database, a user, credentials |
-| another relational store, different vendor | a database — **and not the same one** |
-| a message broker | a virtual host, a user, permissions |
-| an object store | a bucket and keys |
-| an image registry | a repository |
-| an identity provider | a client, a realm, a secret |
-| an analytics service | a site, and a tracking identity |
-| a low-code data platform | a base, and a token |
-| an application platform | a project, which is several of the above at once |
-| a mail server | a mailbox, an alias, credentials |
-
-**Any hosted service can be a factory.** Providing is a facet a module may have, not a kind of
-module it is — which is the same conclusion the effort reached about services and applications,
-arriving from the other direction.
-
-That kills the last reason to keep *provider* as a category. A module runs something, or grants
-something, or both, or neither.
-
-### Two stores, and why `database` still is not a name
-
-Two relational stores from different vendors both grant *a database*. They are the sharpest
-possible test of the substitutability rule from [`proposal.md`](proposal.md), and they fail it
-completely: different wire protocol, different dialect, different driver, different client
-library compiled into the consumer.
-
-A consumer declaring `requires: database` and being handed either would break against one of
-them. So the name promises what no provider delivers — and now with two real providers in the
-catalogue rather than a thought experiment.
-
-*Database* remains a **tag**. It is how a person finds both. It is not how a consumer names what
-it needs.
-
-## The same shape, three more times
-
-The message broker has all nine. So does the object store, and so does the image registry. They
-differ in what a consumer asks for — a database, a virtual host, a bucket, a repository — and in
-nothing structural.
-
-**A substrate service is a service plus a factory.** That is the whole pattern, and there are
-four of them. It generalises past the substrate too: anything that grants something per consumer
-has this shape, and anything that does not is the simpler case.
-
-But two things differ *between* them, and both matter more than the similarity.
-
-### The broker cannot be managed over the broker
-
-[ADR 0002](../../02-DECISIONS/0002-nodes-communicate-over-a-broker.md) makes the broker the
-channel every node takes work from, and
-[ADR 0004](../../02-DECISIONS/0004-a-node-and-how-it-joins.md) makes it the security
-boundary — everything a node applies arrives through it.
-
-So the module providing the broker is also **the way modules are managed**. A declaration cannot
-be delivered to it over itself, and reconfiguring it is done through the thing being
-reconfigured. Nothing else in the catalogue has that property; the store is consumed by the
-control plane but is not how the control plane *reaches* anything.
-
-This is exactly what the carried bundle exists for
-([ADR 0004](../../02-DECISIONS/0004-a-node-and-how-it-joins.md)): the broker is raised from
-what the host carries, before there is a channel, because there is no other way to raise it.
-Recorded here because it is a constraint on *one module*, not a general rule, and a schema with
-no way to say so hides it.
-
-### Two modules of identical shape want different instance counts
-
-The broker is one per mesh — a single point of failure and a single point of trust, by decision
-rather than by accident. The store cannot be: a node that must keep working while disconnected
-([ADR 0004](../../02-DECISIONS/0004-a-node-and-how-it-joins.md)) cannot depend on a database
-somewhere else.
-
-Same nine properties, opposite answers. Which settles something the cases file left open: **how
-many instances is not derivable from what a module is.** It is a decision per module, it has to
-be declared, and nothing in `provides`, `requires` or `excludes` says it.
-
-### And revocation differs in consequence
-
-Revoking a database leaves data behind until something drops it — a leak, and recoverable.
-Revoking a virtual host drops whatever had not been delivered — not recoverable, and silent.
-
-The relation is the same and the blast radius is not, which is an argument for the provider
-deciding what revocation means rather than the mesh applying one rule to all of them.
-
-
-## The assignment is a third thing
-
-Recorded because the operator tried the alternative and abandoned it: **modules were once
-node-agnostic**, and it did not survive contact.
-
-The worked example says why. Several of a provider's nine properties are not properties of the
-module at all:
-
-- **where its state lives** — a volume on a particular machine;
-- **how it is reached** — the same module on two nodes may answer locally, on the network, or
- publicly, and that is a per-assignment decision;
-- **configuration derived from the hardware** — tuning follows the memory and storage of the
- machine it landed on;
-- **whether this instance is the one** a given consumer is provisioned from.
-
-None of those belong in the catalogue, because they differ per node. None belong in the node,
-because they are about this module. **They belong to the pairing**, and a design with only
-modules and nodes has nowhere to put them — which is what "node-agnostic" ran out of.
-
-So there are three entities, not two:
-
-> **a module** · **a node** · **an assignment**, which is a module on a node and carries its own
-> configuration
-
-The current system already has this, arrived at the same way: environment values are stored per
-module *and per node*, so a module's settings differ between the machines running it.
-
-**This does not put placement back in the manifest.** A module still says what must be true of a
-node and never which node ([`proposal.md`](proposal.md)). What changes is that the *result* of
-placing it is a thing with its own state, rather than a fact recorded on one of the two ends.
-
-And it makes the composed declaration question from above answerable: a declaration is built
-from the module, the node's inventory, and the assignment between them. Three inputs, which is
-why two were never enough.
diff --git a/01-RESEARCH/012-the-minimum-viable-node/00-overview.md b/01-RESEARCH/012-the-minimum-viable-node/00-overview.md
deleted file mode 100644
index 6e580db0..00000000
--- a/01-RESEARCH/012-the-minimum-viable-node/00-overview.md
+++ /dev/null
@@ -1,213 +0,0 @@
----
-status: active
-initiated: 2026-08-26
-touches:
- - 02-DECISIONS/0005-the-node-host.md
- - 02-DECISIONS/0011-managed-files-are-generated-never-edited.md
- - 03-DESIGN/01-to-be/05-the-node-host.md
- - 03-DESIGN/00-as-is/05-runtime-and-installation.md
- - 01-RESEARCH/011-the-module-graph/00-overview.md
- - 02-DECISIONS/0078-the-store-and-broker-are-modules.md
- - 02-DECISIONS/0088-the-foundation-filters-before-anything-listens.md
- - 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md
----
-
-# 012 — The minimum viable node, and adopting what is already there
-
-## What is being investigated
-
-Two questions that turn out to be one:
-
-**What is the bare minimum to run a one-node mesh?** Not the tiers as asserted, but the actual
-closure — take the thing that must run, walk what it needs, and the set that comes back is the
-answer.
-
-**And how does a machine that is already in use become that?** A candidate node is not empty. It
-has a package manager, probably a container runtime, possibly a git installation, each with
-configuration somebody chose. The mesh must **own** those, and owning is not the same as finding
-them present.
-
-## Why
-
-Building tier 0 reached a wall that looked like a packaging problem and is not.
-
-The host can be told to run a container or install a package. Both need a file — an image, an
-archive — and the question was where the host gets it. That framing produced a bad trilemma:
-carry everything in the bundle, download at apply time, or have something push the files in
-first. Downloading fails on the first node, which cannot fetch the image registry from the image
-registry it is trying to start.
-
-> **Qualified by [ADR 0006](../../02-DECISIONS/0006-the-substrate-and-the-control-plane.md).** The
-> reframing below still holds for what a *tailored installer* contains — the missing pieces for a
-> given machine. It does **not** have to hold for container images: the installer fetches those
-> by digest, because a real machine has a network and the sealed case is the lab.
-
-**The reframing:** the machine is not offline. What matters is *when* the fetching happens. Move
-it from apply time to **build time** — build the installer on a machine that has a network,
-tailored to the target, and apply it on a target that then needs nothing. That is the same move
-the lab already made for its router image, and the same property the delivery design already
-claims: what ships is self-contained and a deploy touches no network.
-
-Which makes the interesting question not *where do artifacts come from* but **what is missing
-from this particular machine**, and that needs both of the questions above answered.
-
-## What tailoring implies
-
-- **The binary stays generic; the payload is tailored.** One static host per architecture. What
- is machine-specific is the bundle it applies. Rebuilding the host per machine would buy
- nothing.
-- **Detection is the input, not a report.** What the host already reports about a machine —
- its profile and inventory — is what the difference is computed against. This is the first use
- of stage 1 by something other than a person reading it.
-
-## Adoption
-
-**Simply having a package installed is not enough.** If the mesh manages the container runtime,
-it decides that runtime's configuration; a runtime found already installed carries settings
-somebody chose, and those cannot be discovered by noticing the binary exists.
-
-So a machine already in use is **adopted**: what is there is read, taken over, and thereafter
-generated.
-
-**This was the original path.** [`00-as-is/05`](../../03-DESIGN/00-as-is/05-runtime-and-installation.md)
-records adoption of a pre-existing machine's configuration as the original mechanism, since made
-legacy and explicitly out of scope for the lab. It returns here for a different reason than it
-was dropped for, which is a thing to notice rather than to gloss.
-
-**It creates a state that does not exist today.** [ADR 0011](../../02-DECISIONS/0011-managed-files-are-generated-never-edited.md)
-has managed files generated and never edited; adoption needs a one-time import before that rule
-starts applying. Three states, and the middle one is new:
-
-> unmanaged → **adopted once** → generated
-
-**And it crosses a boundary just drawn.** [ADR 0005](../../02-DECISIONS/0005-the-node-host.md)
-says the host never touches what it did not create — the rule that stops a converger deleting
-what the mesh never put there. Adoption is the deliberate act of taking ownership of exactly
-that. The rule needs a companion rather than an exception: *never, unless adoption made it the
-host's*, with adoption being explicit, recorded, and visible in what the host says it owns.
-
-## Nothing is taken over without keeping what was there
-
-**Before adoption touches a file, the original is kept.** Adoption happens on machines somebody
-is already using, and the configuration being taken over is configuration somebody chose. A
-one-way door on a working machine is not an installation, it is a risk nobody agreed to.
-
-This is a *never* rule rather than a courtesy, and it earns that by the same incident the mesh's
-strongest rule already carries: the worst loss in this record came from a tool acting on a path
-it did not own. Adoption is that act, made deliberate — which makes the safeguard obligatory
-rather than optional.
-
-What that requires, and what remains open: where the copy lives, whether it is recorded in what
-the node knows about itself so that adoption is *visibly* reversible, and whether the mesh keeps
-it forever or hands it back when it stops managing the thing.
-
-## Adoption produces a briefing, not just a result
-
-Proposed by the operator, and it answers a question this effort had open with two bad answers.
-
-Adoption meets things a script cannot decide. A container runtime configured with one storage
-driver and a mesh wanting another. A package pinned to a version somebody chose for a reason.
-Local settings the mesh has no opinion about and no business discarding. Silently winning is
-wrong in both directions; refusing outright makes a machine in use unadoptable.
-
-**So adoption has two outputs.** What it did — mechanical, recorded, in the node's state. And a
-**briefing**: what it found, what it took over, and what it could not resolve, written to be
-read by a person or an agent, which is the first thing a session on that node has to work with.
-
-Conflicts are **flagged, not resolved**. That is the same principle the declaration parser
-already applies — name every problem at once, to somebody who can act on it — at a larger
-scale, and applied to a case where refusing wholesale would be worse than proceeding.
-
-### On conflict, the machine's configuration is kept
-
-Decided by the operator, after first deciding the opposite — recorded that way because the
-reasoning for each direction is the useful part.
-
-Where the existing configuration and the mesh's disagree, **what is already on the machine
-stays**, the conflict is flagged, and it is reconciled afterwards. Adoption always **completes**
-— flags inform, they do not block — and *adopted with open questions* prevents nothing. The
-node is a node.
-
-**What this buys.** Adoption becomes non-destructive by construction. The class of conflict that
-made the opposite rule dangerous — a storage driver against the filesystem it is actually on, a
-data directory pointing at a mount that exists — cannot arise, because nothing tied to the
-machine's physical reality is ever overwritten. A machine in use keeps working exactly as it
-did.
-
-**What it exposes, which is the mirror of what it fixes.** The mesh's configuration is not only
-preference. Some of it is what a module needs in order to function at all. Keeping the machine's
-version there produces a module that is installed and does not work — *an installed package is
-not a capability*
-([04-ISSUES/007](../../04-ISSUES/007-an-installed-package-is-not-a-capability/00-report.md))
-arriving from a direction that issue did not anticipate. And a fleet where every node kept its
-own settings is a fleet where a module works on one node and fails on another with nothing in
-the mesh able to say why.
-
-**The distinction that dissolves both rules.** Neither direction is right as a blanket, because
-the question is not *whose configuration wins*. It is whether the module **requires** the
-setting or merely **prefers** it — required contradictions cannot be kept without breaking the
-module, and preferences should always yield to what is already there.
-
-That is a property of the module's own declaration rather than of the adoption algorithm, which
-makes it one more thing the graph would carry
-([research 011](../011-the-module-graph/00-overview.md)). Until modules can say which of their
-settings are load-bearing, adoption is choosing a default in the dark, and the default chosen
-here is the one that does not break the machine it is adopting.
-
-### The briefing carries an outcome, and the outcome is derived
-
-Proposed by the operator: the report states plainly whether adoption succeeded, partly
-succeeded or failed, and each line carries its own severity.
-
-**Each line is marked, and the overall is the worst mark present.** Derived rather than stated
-alongside, because two fields written independently drift — and a briefing reading *full
-success* while carrying a failed line is exactly the fault this record keeps cataloguing. An
-outcome computed from its lines cannot disagree with them.
-
-| Mark | Means |
-|---|---|
-| `ok` | done, and verified |
-| `kept` | a disagreement; the machine's value was kept and somebody should look |
-| `unknown` | could not be determined |
-| `failed` | could not be done — the node is not what was asked for |
-
-**`unknown` is not a shade of success.** Adoption will meet configuration it cannot parse and
-state it cannot read, and folding those into *fine* is the same move as reporting an installed
-package as a capability. A thing nobody could determine is a thing nobody can rely on, and it
-gets its own mark for the same reason a capability detector reports *why*.
-
-**And this opens something the earlier rule did not cover.** *Flags inform, they do not block*
-was decided about **conflicts** — where the mesh chose, deliberately, and the machine still
-works. A **failure** is different in kind: not *we chose* but *we could not*. Treating both the
-same makes a node where something the mesh needed never happened indistinguishable from one
-where a log level differed. Whether a failed line still lets adoption complete is therefore
-reopened by adding severity, and is not decided here.
-
-## Adoption as a mode, for the migration
-
-*2026-09-22.* The migration from the predecessor mesh gave the middle state a length. A machine
-running the predecessor is **adopted** when the mesh comes up on it — the predecessor's control
-stopped, the machine's firewall and files kept in force, the mesh opening what it needs through
-them — and stays adopted while its modules migrate one at a time, until the operator **converges**
-it. Measured on the control-node, and weighed against the alternatives, in
-[*migrating a node that is in use*](migrating-a-node-in-use.md); decided in
-[ADR 0100](../../02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md).
-
-## Open questions
-
-| Question | Why it is open |
-|---|---|
-| What is the closure for a one-node mesh? | The skeleton asserts four pinned services. [Research 006](../006-mesh-from-scratch/00-overview.md) already asks whether it is four or five and does not answer. A graph gives a computed answer instead of an asserted one, which is [research 011](../011-the-module-graph/00-overview.md). |
-| Is "tier" the same thing as a graph level? | Tiers were named as a bootstrap order. If the closure is computed, tiers may be a derived view of the graph rather than a separate concept — or they may be a coarser boundary that survives for a different reason. |
-| ~~What happens when existing configuration contradicts what the mesh needs?~~ | **Answered** — the machine's configuration is kept, the conflict is flagged, and it is reconciled afterwards. |
-| ~~Do flags block, or only inform?~~ | **Answered** — they inform. Adoption always completes, and the node is a node. |
-| Can a module say which of its settings are load-bearing? | The question that dissolves the conflict rule rather than choosing a side. A setting the module *requires* cannot be kept from the machine without producing something installed and broken; a setting it merely *prefers* should always yield. Until a module can say which is which, adoption is defaulting in the dark. Belongs with the graph. |
-| Does a `failed` line still let adoption complete? | *Flags inform, they do not block* was decided about conflicts, where the mesh chose and the machine works. A failure is *we could not*, which is different in kind — and treating them alike hides the worse one behind the commoner one. |
-| How is a flagged conflict reconciled, and by whom? | The briefing hands it to a session. What that session is empowered to change, and whether the resolution is recorded so the next adoption does not re-raise it, is undecided. |
-| ~~How long is a machine adopted?~~ | **Decided** ([ADR 0100](../../02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md)) — for as long as it is being migrated: a mode per node, recorded, ended by an explicit and previewed flip. |
-| Where does the kept original live, and for how long? | Whether it is recorded in the node's state so adoption is visibly reversible, and whether it is returned when the mesh stops managing the thing. |
-| What shape is a briefing? | Structured enough to be acted on, prose enough to be read. It is the first thing a session on a new node sees, which makes it an interface rather than a log. |
-| Does owning a package mean owning its version? | Owning configuration and owning the package are different scopes. The second means the mesh decides which version is installed, and that decision then has to survive the machine's own package manager updating it. |
-| How does a bundle stay true between building and applying? | It is built against a scan of the target. The machine can move between the scan and the apply, so the host has to verify rather than assume — and fail plainly when the bundle no longer fits. |
-| What cannot be precomputed at all? | Anything built from source on the target still needs a toolchain and a network at that moment. Tailoring moves that cost rather than removing it, and *minimal viable* has to be honest about what it cannot ship ahead. |
-| Does presence differencing understate the gap? | Knowing a package manager is installed does not say it is the version the mesh needs. A difference computed on presence alone is optimistic. |
diff --git a/01-RESEARCH/012-the-minimum-viable-node/migrating-a-node-in-use.md b/01-RESEARCH/012-the-minimum-viable-node/migrating-a-node-in-use.md
deleted file mode 100644
index ee05d806..00000000
--- a/01-RESEARCH/012-the-minimum-viable-node/migrating-a-node-in-use.md
+++ /dev/null
@@ -1,154 +0,0 @@
-# Migrating a node that is in use — adoption as a mode, not a moment
-
-*2026-09-22. Measured on the machine that will be the control-node, which is running the
-predecessor mesh today. Nothing below names it; the counts are its own.*
-
-## The question
-
-The mesh replaces a predecessor mesh that is running, on the same machines, with the services
-people use. The control-node is decided: it is the machine that already carries the predecessor's
-broker and build pipeline. So the question is not *where* the mesh starts but **how a machine
-running the predecessor becomes a node of the mesh without its services noticing** — and then how
-each of the other machines follows.
-
-## The operator's proposal
-
-Proposed by the operator, 2026-09-22, and the shape this document tests:
-
-1. **Stop the predecessor's control on a machine** — its daemons that write configuration: the
- network and firewall configuration above all, which decide what is reachable and what is
- blocked. Its services keep running; only the control over their configuration stops.
-2. **Bring the mesh up on that machine in adoption mode.** It takes custody of those files and
- keeps what it finds in force.
-3. **Migrate the modules one at a time**, data preserved, per the cutover procedure.
-4. **Move to the next machine and repeat** — adopted first, keeping its local configuration, then
- migrated.
-5. **When every machine is migrated, flip adoption mode**, and the mesh takes full control of the
- configuration it has been holding.
-
-This is the research above made concrete. It keeps the conflict rule already decided here — *on
-conflict, what is on the machine stays* — and gives the middle state, *adopted*, a length: not a
-one-time import before generating starts, but a mode that lasts for as long as the machine is
-being migrated, ended by an explicit act.
-
-## What the machine actually looks like
-
-Measured, read-only:
-
-| | |
-|---|---|
-| Containers running | 60, all the predecessor's services and their stores |
-| The predecessor's control | user-level daemons, separate from the services; none of the 20 running system services is the predecessor's control |
-| Firewall | the predecessor's, active: 54 incoming rules and 52 forwarding rules, each served port allowed explicitly — how a default-deny firewall reads |
-| Files the predecessor's configuration sync writes | 12, of which 3 are system files (an ssh server drop-in, the package manager's configuration, one service's configuration); the rest are the operator's shell and agent files |
-| Per-service configuration | environment and composition files per service, written by the predecessor's service tooling |
-
-**Stopping the predecessor's control stops nothing that serves.** The services are containers and
-system units that run without it; what stops is the rewriting of their configuration. Nothing
-changes on the machine until something else writes.
-
-## What collides, measured rather than assumed
-
-An earlier note assumed the mesh's foundation could not stand beside the predecessor because both
-want the store's and the broker's standard ports. **The measurement says otherwise.** The
-predecessor publishes its own store on a non-standard port and its broker on another; the standard
-ports the foundation binds for its store and its bus are free.
-
-What does collide:
-
-| The mesh wants | Held by | When |
-|---|---|---|
-| the registry's port | the predecessor's registry | at genesis — the foundation raises a registry |
-| the broker's management port, on loopback | the predecessor's broker | at genesis |
-| the private network's port | the predecessor's own tunnel | at genesis on a control-node that is the private network's hub; otherwise when the node is placed on it |
-| the web ports | the predecessor's reverse proxy | when the route proxy is assigned |
-| the resolver's port | a resolver the predecessor runs | when a resolver module is assigned |
-
-On the control-node, which is the private network's hub, three of these fall at genesis and cannot
-be deferred; two only when a particular module is taken, the moment its predecessor stops anyway.
-Today the foundation's ports are **fixed**: written in the installer's bundle and in the
-catalogue's manifests, so a collision is found when a container fails to bind, not before — and a
-port changed at genesis would be changed back when the foundation is adopted as modules, since the
-applier recreates a container whose declared spec differs. The private network's address range
-must also stay clear of the range the predecessor's tunnel uses; on the machines measured they are
-distinct.
-
-## What would break if the mesh came up as it is today
-
-**The firewall.** Genesis loads a base ruleset — drop anything undeclared — in its own table
-([ADR 0088](../../02-DECISIONS/0088-the-foundation-filters-before-anything-listens.md)). The
-predecessor's firewall is a different table. The kernel runs every base chain registered at the
-same hook, in priority order: an accept ends only its own chain and the packet goes on to the next,
-and a drop in any is final — whether the other firewall's chains are nftables or legacy iptables.
-So the
-mesh's base ruleset would drop everything the predecessor's firewall allows and the mesh has not
-declared — every web, mail and database port in the table above — the moment genesis ran.
-
-**The files.** The host writes a declared file whatever it finds at the path, reporting it as
-updated. A file the predecessor left — the ssh server drop-in, the resolver's configuration — is
-replaced the first time a module declaring that path is assigned, before that module's service
-has moved.
-
-**The container names.** The applier keys a container on its name. A catalogue module whose
-container carries the same name as the predecessor's service it replaces takes that container over
-the moment it is assigned: today, *assigning a module is migrating it*, never a preparation.
-
-**The published ports.** The foundation publishes its ports on every interface, and a published
-container port reaches the container through the forwarded path, not the incoming one. A firewall
-that filters only incoming traffic never sees it. The base ruleset is what keeps the store
-unreachable from outside today — and it is the thing that cannot be loaded on this machine.
-
-## The pipeline freezes while the control-node migrates
-
-The predecessor's build pipeline and its coordinator run on the control-node. Stopping its control
-there stops the predecessor's updates for **every** machine it manages, until the migration is
-done. Their services keep running; they receive nothing new. That is the price of the proposal and
-it is worth stating, not a reason against it: the migration is the period in which the predecessor
-is being replaced, and it does not need to keep changing.
-
-## What adoption mode has to mean
-
-For the proposal to hold, *adopted* must be a **state the mesh records per node**, not an
-intention, and each thing the mesh would otherwise take must say what it does in that state:
-
-- **What is found is kept until its module is taken.** *Found* is precise: present at a declared
- path or name with no record in the host's store. A found file or container is held, its original
- recorded, until the operator **takes** the module on that node — the cutover, done when the
- module's data has moved. Assigning prepares; taking migrates. Without the distinction the rule
- never fires: the host only ever sees what assigned modules declare.
-- **The firewall found on the machine stays in force.** The mesh loads no table on an adopted node
- that drops by default or holds an accept. What it needs open it declares as openings the host converges
- *through the found firewall*, on the incoming and the forwarded path, marked as the mesh's and
- re-checked on every reconcile so a reload or reboot does not lose them. An accept in a table of
- its own would not help: the found firewall's drop would still be final. What a table of its own
- *can* do is refuse, and a refusal is final too — so the mesh guards the store and the broker's
- management port from everyone but the private network and the machine itself, in a table that
- only refuses, ahead of the container runtime's redirect — which the found firewall does not do
- and cannot undo. The bus, the registry and the hub's port stay open to
- anywhere: a node enrols before it has a private-network address.
-- **The foundation's ports are the node's to give** — set at genesis, checked free, and kept as
- that node's settings, read everywhere they are used, so adopting the foundation as modules does
- not move them back.
-- **The flip is per node and previewed** from what is actually reachable — listening sockets and
- published ports, not the found firewall's allow list, which does not see what a container runtime
- forwards.
-
-## Options weighed
-
-| Option | Verdict |
-|---|---|
-| Cut the machine over in one go: stop the predecessor's store, broker, registry and proxy, raise the foundation in their place | Rejected. Every predecessor service goes down until it has migrated, and the predecessor's other machines lose their broker. The rollback is restarting the predecessor, which is a recovery, not a step. |
-| A separate machine as the control-node | Rejected. Contradicts the decision that the control-node is the machine that already carries the predecessor's broker and pipeline. |
-| Converge on joining, as the mesh does today | Rejected. The base ruleset closes every predecessor port at genesis, and found files are replaced before their services move. |
-| Make only the foundation's ports configurable and otherwise converge | Rejected as insufficient. It solves the bind collisions and none of the firewall or file ones. |
-| Treat assigning a module as migrating it | Rejected on review. The rule that keeps found files would never fire — the host only sees what assigned modules declare — and an assignment on an adopted node would be an outage rather than a preparation. |
-| **Adoption as a mode, per node, ended by an explicit flip** | The proposal. It is the conflict rule already decided here, given a duration. |
-
-## What this leaves open
-
-Answered by the decision record this feeds — [ADR 0100](../../02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md)
-— only for the migration's needs. The general questions above stay open: whether a module can say
-which settings are load-bearing, what shape a briefing takes, how a flagged conflict is reconciled.
-One is newly sharp: the mesh opening ports through a firewall it did not install needs to speak that
-firewall. There is one kind on the machines measured; a machine with another is not covered until
-someone writes for it.
diff --git a/01-RESEARCH/013-the-forge-and-the-registries/00-overview.md b/01-RESEARCH/013-the-forge-and-the-registries/00-overview.md
deleted file mode 100644
index 7f2db0d1..00000000
--- a/01-RESEARCH/013-the-forge-and-the-registries/00-overview.md
+++ /dev/null
@@ -1,66 +0,0 @@
----
-status: graduated
-initiated: 2026-09-23
-touches:
- - 02-DECISIONS/0075-two-stores-and-which-provides-what.md
- - 02-DECISIONS/0079-the-foundation-seats-are-named-after-their-servers.md
- - 02-DECISIONS/0071-genesis-builds-from-a-mesh-that-already-exists.md
- - 02-DECISIONS/0082-the-registry-is-reached-by-name-and-trusted-by-the-overlay.md
- - 04-ISSUES/085-the-packages-port-given-at-genesis-is-not-a-setting/00-report.md
- - 04-ISSUES/090-the-forge-module-does-not-take-over-the-forge-genesis-raised/00-report.md
-became:
- - 02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md
- - 02-DECISIONS/0239-a-delivery-is-owned-by-the-mesh-delivery-module-and-runs-from-commit-to-delivered.md
- - 02-DECISIONS/0267-an-artifact-is-rebuilt-only-when-its-build-source-changes-and-a-module-moves-only-along-build-dependencies-on-artifacts.md
- - 03-DESIGN/01-to-be/47-delivery-from-commit-to-delivered.md
----
-
-# 013 — The forge and the registries: what a seat is for, and what it is not
-
-**The question, asked during the first migration:** the mesh runs a forge that serves git and
-packages, an image registry, and a bootstrap forge genesis raises before any module exists. Should
-there be a mesh-scoped **git seat**, the way there is one for the store and the broker?
-
-**The short answer: no, and the question points at a different hole.** A seat answers *is there
-exactly one of you*. Every symptom around the forge is about **an address and a credential nobody
-resolves**, which a seat does not answer. The mechanism that would answer it — a provision, with
-`serves` and a grant — already exists, is already used for the image store, and is already
-declared for packages. It simply has no consumer: the one module that needs it carries a literal
-instead.
-
-See [the survey](the-survey.md) for what the code does today, with evidence.
-
-## What was found
-
-- **A seat does exactly two things**: it refuses a second claimant at resolution, and in one place
- it answers *where is the broker*. It is a resolution-time predicate; no host ever hears of it.
-- **Four mesh-scoped seats exist**, all named after a singular server the mesh runs **for its own
- working** — controller, store, broker, catalogue. A forge is an application the world made, not
- part of that set.
-- **Nothing in the mesh requires git.** There is no provision for it, and the forge's git service —
- over https and ssh — is declared nowhere. Only its npm half is a provision.
-- **`package-registry` is a fully built provision with zero consumers.** The builder, its only
- real consumer, bypasses it with a file naming the module, the address `127.0.0.1` and the port.
-- **The builder is told where source lives, per build, by a person.** The clone URL is an opaque
- string; each module remembers its own; no credential is ever attached; nothing polls a forge.
-- **A seat would forbid something the mesh should allow**: a second forge — one for the mesh's own
- source, one for something else — which ADR 0075 already argues for against a node-scoped claim.
-
-## What this leaves open
-
-- Should the forge's git service become a **provision** (`source-forge`), so the builder resolves
- the address and a credential the way it already resolves the image store? That is what would let
- a forge move, be renamed, or be replaced without editing every module's stored URL, and it is the
- unanswered half of [issue 085](../../04-ISSUES/085-the-packages-port-given-at-genesis-is-not-a-setting/00-report.md).
-- The obstacle is known and written down: such a requirement **may go unanswered during genesis**,
- and the mesh has no optional requirement. Deciding that is the real work.
-- Should the mesh learn when a source moves? It records the fact and never discovers it: nothing
- polls, and there is no receiver for a forge's push. `build --behind` answers a question only a
- person can currently make true.
-
-## What it became
-
-*Closed 2026-10-10.* The forge became the `git` seat ([ADR 0111](../../02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md)): a source on it is recorded by its path and seat, never
-an address, and an unheld seat refuses self-hosted builds and nothing else, which settles the genesis
-concern. The mesh learns of a change from the forge's pull requests, each one a delivery
-([ADR 0239](../../02-DECISIONS/0239-a-delivery-is-owned-by-the-mesh-delivery-module-and-runs-from-commit-to-delivered.md), [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)), designed in [to-be 47](../../03-DESIGN/01-to-be/47-delivery-from-commit-to-delivered.md).
diff --git a/01-RESEARCH/013-the-forge-and-the-registries/the-survey.md b/01-RESEARCH/013-the-forge-and-the-registries/the-survey.md
deleted file mode 100644
index ab79202b..00000000
--- a/01-RESEARCH/013-the-forge-and-the-registries/the-survey.md
+++ /dev/null
@@ -1,130 +0,0 @@
-# The survey — seats, the forge, the registries, as the code has them
-
-*2026-09-23. Read from the code and the records, not from memory. Every claim here was checked
-against a file.*
-
-## What a seat is
-
-The glossary calls a seat *a named position at a scope with a capacity*, and a claim *a module
-taking a spot on it*. **Capacity does not exist in the code.** There is no capacity field and no
-bench; every claim is exclusive. The "shared seat" of the design is the word `provides` wearing
-that name.
-
-Claiming does two things and no others:
-
-- **It refuses a second claimant when a node's modules are resolved** — within one node at any
- scope, and against every other node for mesh and site scope.
-- **It answers one question, once:** the controller finds where the broker is by looking for the
- module claiming the broker's seat.
-
-No host is ever told about a seat. It is a resolution-time predicate.
-
-Two mechanical asymmetries matter. A mesh-scoped seat's exclusivity is defended only by nodes
-that *resolve*: a node whose modules fail to resolve contributes nothing, so a broken node does
-not hold its seat against a second claimant. And a site-scoped seat does nothing at all when
-either node has no site.
-
-## The seats that exist
-
-Twelve claims, twelve names. Four are mesh-scoped: the controller, the store, the broker, the
-catalogue. All four are things the mesh runs **for its own working**, and three of them are raised
-by genesis and adopted in place — which is the argument of the record that named them.
-
-The node-scoped ones split in two: a **role on this machine** (the build machine, the packet
-filter, the intrusion prevention, the private network) and **a scarce machine resource** (the
-resolver's configuration file, the DNS port). The second kind is a claim for the reason a port is:
-there is one of it on the machine.
-
-Two oddities worth stating:
-
-- The image store claims its seat at **node** scope while providing its service at **mesh** scope.
- The record that discussed this left open whether it should hold that claim at all.
-- **Nothing claims the two public ports.** The proxy binds them and claims nothing, which is the
- hole the route handover fell into.
-
-## The forge, and what it serves
-
-The forge serves three things: git over https, git over ssh on its own port, and an npm registry.
-**Only the npm half is in the mesh's vocabulary** — the forge provides `package-registry` and says
-where it answers. Git is served and declared nowhere: no provision, no `serves`, nothing that can
-require it. The only trace is the public label in its route contribution, which the mesh is
-explicitly not meant to interpret.
-
-The image store is a separate module and a separate provision, plain HTTP, trusted because it is
-reachable only over the private network.
-
-A second module also provides `package-registry`. Two providers of one provision is not a refusal
-but an ambiguity, and the mesh already has the command that settles it: pin one.
-
-## Where the builder's three addresses come from
-
-| What it needs | How it finds it |
-|---|---|
-| the broker | a sealed secret of its own |
-| the image store | **a real provision binding**, resolved by the mesh |
-| the package registry | **a file in its own manifest**, naming the module, `127.0.0.1` and the port |
-| the source repository | nothing at all — a person types a clone URL per build |
-
-The installer says this plainly in a comment: *the one binding nothing resolves: the builder dials
-the forge by a number it carries.* Genesis can override the port of that literal, and nothing
-else: not the forge's identity, not its address.
-
-`package-registry` therefore has **zero consumers**. The builder does not require it. The
-provision is fully built — two providers, grants, a served description including the registry's
-path — and nothing asks for it.
-
-## How a module's source is found
-
-It is not found; it is supplied. The clone URL arrives as an argument, travels to the build machine
-and reaches `git clone` unexamined. No credential is attached, so a private forge works only if the
-build machine's own git configuration already authenticates. Each module remembers the URL it was
-built from, so there are as many forge addresses as modules, each frozen at whatever was typed.
-
-At genesis there are four independent source URLs, one per flag, unrelated to each other.
-
-If the forge moves or is renamed: every stored URL goes stale independently, each failing at its
-own clone, and there is no command to re-point them. Genesis keeps working, because it takes URLs
-as flags — which is the existing record's position: the forge is reached *by a name outside the
-mesh*, and failover is repointing that name.
-
-## What genesis's forge is
-
-A bare container run before any module exists, because the toolchain resolves the mesh's own
-packages by version from an npm registry. It holds **no seat, no manifest, no provision**. The
-mesh is told one thing about it — a port — and only when that port is not the default. **On an
-ordinary genesis the mesh never learns the bootstrap forge exists.**
-
-Its successor module differs from it in five ways: container name, network, data directory, the
-address it is told to call itself, and — contradicting a comment that says they are pinned
-identically — **the image digest**. So assigning the module raises a second forge beside the first.
-
-A seat cannot fix that. A seat is checked between manifests; the bootstrap forge has no manifest,
-so no seat can see it. The check that is actually wanted — same name, same data, same network — is
-between an installer constant and a manifest.
-
-## Would a git seat be coherent?
-
-In form, yes: add the claim and a second forge is refused. In effect it buys one refusal nobody
-has hit, and forbids an arrangement the mesh should allow — a second forge for something other
-than the mesh's own source.
-
-Measured against the four mesh seats, it does not fit: those are singular servers the mesh runs
-for itself, and a forge is an application. Nothing would *use* it, because the only seat consumer
-answers "where is the broker", and a forge address is not one fact: a module's source is a
-repository **and a path and a ref**.
-
-## What the code already supports and nobody uses
-
-- **`package-registry` with no consumers.** The builder's literal file is a hand-rolled copy of
- the binding the mesh would have written for it.
-- **Pinning a provider** already expresses *this* forge, of several, without forbidding the second.
-- **Grants** on the forge are the credential mechanism that would hand the builder an account.
- Today the installer creates that account directly, because nothing requires the provision and so
- the grant has no consumer to write for.
-
-## The reading
-
-The question *should there be a git seat* is the wrong shape for the problem under it. Every
-symptom — the literal address, the port that would not follow a node, the takeover that is not a
-takeover — is about **an address and a credential nobody resolves**. A seat resolves neither. The
-provision mechanism does, and is already built.
diff --git a/01-RESEARCH/014-the-bus-on-nats/00-overview.md b/01-RESEARCH/014-the-bus-on-nats/00-overview.md
deleted file mode 100644
index bcaa4e1c..00000000
--- a/01-RESEARCH/014-the-bus-on-nats/00-overview.md
+++ /dev/null
@@ -1,108 +0,0 @@
----
-status: graduated
-became: 02-DECISIONS/0106-the-bus-is-nats.md
-initiated: 2026-09-23
-touches:
- - 02-DECISIONS/0002-nodes-communicate-over-a-broker.md
- - 02-DECISIONS/0033-the-substrate-is-a-store-and-a-broker.md
- - 02-DECISIONS/0039-what-the-sdk-holds-and-refuses.md
- - 02-DECISIONS/0041-events-are-a-relationship.md
- - 02-DECISIONS/0042-the-shape-of-an-event-on-the-wire.md
- - 02-DECISIONS/0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md
- - 02-DECISIONS/0078-the-store-and-broker-are-modules.md
- - 02-DECISIONS/0083-one-push-leaves-the-mesh-consistent.md
----
-
-# 014 — The bus on NATS: replace the broker, and when
-
-**The question, asked mid-migration:** the operator wants the mesh's bus — today an AMQP broker —
-replaced by NATS, with everything the broker does today. Do we finish the migration on AMQP and
-move to NATS after, or go to NATS directly?
-
-**The short answer: decide NATS now, build it in the lab in parallel, and cut the mesh's bus over
-in one rehearsed rollout — after the migration's core is done and never underneath it.** "First
-everything on AMQP" is already the state and costs nothing more: the predecessor's broker was
-merged into the mesh's tonight, and every module converted from here on targets the sdk's broker
-contract, which names no protocol. The only code that speaks AMQP is the mesh's own, in three
-places, and it is swapped once.
-
-## What was measured
-
-**Where AMQP is spoken** (non-test files): the controller's `internal/link` (5 files, ~2.4k lines
-with tests) and the builder's main; the host's `internal/link` (~1.5k); the tool runtime's
-`broker-amqp.ts` (~600 with its main). **The sdk speaks none** — its `Broker` is `request`,
-`handle`, `publish`, `subscribe`, `close` ([ADR 0039](../../02-DECISIONS/0039-what-the-sdk-holds-and-refuses.md)
-put the client in the runtime for exactly this). **Every module's tools and events go through the
-sdk**; one module (`amqp-ping`, a probe) talks AMQP on purpose.
-
-**What the bus carries:** a control queue (`control`, `.upgrades`, `.catchup`), one queue per node
-(`node.`), `builds`, the enrolment/report/alive/built flows, a topic exchange of events
-(`mesh.events..`, dead-lettered to `mesh.events.dead`), an RPC exchange (`mesh.rpc`)
-and per-tool service queues (`serve..`), plus an MQTT exchange.
-
-**What it relies on:** TLS for the bus a node enrols over; prefetch with reject/nack so the control
-queue **holds messages unacknowledged while the store restarts** and retries
-([ADR 0083](../../02-DECISIONS/0083-one-push-leaves-the-mesh-consistent.md)); publisher confirms and
-mandatory routing; a dead-letter exchange; **per-module accounts scoped by `emits`/`consumes`**
-([ADR 0043](../../02-DECISIONS/0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md)),
-minted through the broker's management API; the broker as a module holding the `mesh-broker` seat
-([ADR 0078](../../02-DECISIONS/0078-the-store-and-broker-are-modules.md)).
-
-**The predecessor's world**, now on the same broker: ~50 AMQP connections per machine from four
-machines, 595 queues, four vhosts, six users. All of it AMQP, all of it retiring module by module.
-NATS speaks no AMQP: that world cannot move; it does not need to.
-
-## How NATS answers each of those
-
-| The mesh relies on | NATS | Note |
-|---|---|---|
-| topic routing keys | subjects with wildcards | same shape (`mesh.events.>`) |
-| RPC and tool invocation over an exchange + reply queue | request/reply, native | simpler than today |
-| competing consumers | queue groups | same |
-| durable control queue, hold-unacked-and-retry, catch-up | JetStream: streams, durable consumers, ack/nak with delay, replay | the 0083 guarantee moves to JetStream; core NATS alone is at-most-once and would not do |
-| dead-letter | max-deliver + advisories, or a stream fed from them | different mechanism, same effect |
-| TLS bus | TLS | same |
-| accounts scoped by emits/consumes, vhosts | accounts (isolation) with users and per-subject publish/subscribe permissions | stronger than today; the controller writes an auth config the host declares and the server reloads, instead of calling a management API |
-| management API | `nats` CLI and an HTTP monitoring endpoint | no vhost concept — accounts instead |
-| MQTT | built in | same |
-| a module seat `mesh-broker` | unchanged — the seat is the server, the module changes | ADR 0079 |
-| multi-node | clusters and leaf nodes | not needed now; a leaf per node is a later question |
-
-Nothing the mesh needs is missing. The differences are in the shape of durability (JetStream must
-be declared, streams and consumers are objects) and of accounts (configuration, not API calls).
-
-## The cost, honestly
-
-The bus is the mesh's nervous system. Moving it means: a new `nats` module in the catalogue taking
-the `mesh-broker` seat; the controller's and the host's link packages rewritten; the tool runtime's
-client swapped behind the unchanged contract; enrolment, reports, builds and the guard's ports
-re-derived; the lab beds that prove the bus (store window, enrolment, upgrades) re-run on the new
-one; and eight decisions amended or superseded. Weeks, not days — and none of it can be done
-halfway on a live mesh: the controller, every host and every tool runtime move together.
-
-## The sequencing question, answered
-
-1. **NATS first, directly.** Stalls the migration for the length of the build; the mesh's bus
- changes under a half-migrated node; the predecessor's clients still need AMQP, so a second
- broker runs anyway, plus a bridge for whatever crosses. Rejected.
-2. **Finish on AMQP, NATS after.** Wastes nothing — no module written from here on speaks AMQP —
- but leaves the decision unmade while modules are written, and the runtimes idle. Adequate.
-3. **Decide NATS now; build it in the lab in parallel; cut the mesh's bus over in one rehearsed
- rollout after the core is migrated.** The predecessor's clients never notice: their broker is
- the one the mesh adopted, kept as a compatibility module with an end date — the day the last
- AMQP client is gone. **Recommended.**
-
-## What this leaves open
-
-- The **decision itself**, as a record: the bus is NATS; the AMQP broker becomes the predecessor's
- compatibility broker and retires with the last AMQP client. Written when the operator says so.
-- **JetStream's shape for the control plane**: one stream per concern (control, nodes, builds,
- events) or one with subjects; retention; what the store window guarantee looks like as ack-wait
- and nak-delay. Measured in the lab, not designed on paper.
-- **Accounts as configuration**: the controller writes users and permissions into a file the host
- declares, reloaded on change — which is the [ADR 0102](../../04-ISSUES/102-an-address-recorded-at-genesis-or-build-does-not-follow-the-nodes-ports/00-report.md)
- discipline applied from the start — or the JWT/operator model. The first is simpler and matches
- how the mesh already writes everything.
-- **The MCP bridge** the operator asked for the same day is written against the sdk's contract, so
- it moves with the bus and is not written twice.
-- Whether a **leaf node per machine** replaces the hub-and-spoke bus later — out of scope here.
diff --git a/01-RESEARCH/015-the-object-store-after-minio/00-overview.md b/01-RESEARCH/015-the-object-store-after-minio/00-overview.md
deleted file mode 100644
index a4d3633d..00000000
--- a/01-RESEARCH/015-the-object-store-after-minio/00-overview.md
+++ /dev/null
@@ -1,144 +0,0 @@
----
-status: graduated
-became:
- - 02-DECISIONS/0288-the-object-store-stays-on-the-maintained-fork-and-garage-is-chosen-only-after-the-consumers-s3-features-are-counted.md
-initiated: 2026-09-24
-touches:
- - 02-DECISIONS/0028-the-substrate-supplies-the-control-plane-and-nothing-else.md
- - 02-DECISIONS/0033-the-substrate-is-a-store-and-a-broker.md
- - 02-DECISIONS/0048-a-provider-creates-the-credential-the-mesh-minted.md
- - 02-DECISIONS/0049-a-consumers-identity-fits-the-tightest-backend.md
- - 02-DECISIONS/0078-the-store-and-broker-are-modules.md
- - 02-DECISIONS/0084-which-provider-serves-a-consumer.md
- - 03-DESIGN/00-as-is/03-provisioning.md
- - 03-DESIGN/01-to-be/07-the-foundation.md
- - 04-ISSUES/113-the-object-stores-images-were-withdrawn-upstream/00-report.md
----
-
-# 015 — The object store after MinIO: which S3 implementation, and how the data moves
-
-**The question.** The mesh's object store is MinIO. Its community edition is archived upstream,
-its server and client images have been deleted from every public registry, and the pinned release
-is four and a half years old and will never be patched
-([issue 113](../../04-ISSUES/113-the-object-stores-images-were-withdrawn-upstream/00-report.md)).
-Which S3-compatible implementation replaces it, and what is the migration track for the data and
-the provisioning model that sit on top of it?
-
-**Why now, and why not sooner.** Nothing is on fire: nodes that already hold the images keep
-running, and issue 113 establishes that the deploy path tolerates an unfetchable-but-present
-image by design. The forcing function is not an outage but a one-way door — **no node that does
-not already hold the images can ever provision the module again**, so the mesh's ability to stand
-a node up from its declarations is already broken for this module, and silently.
-
-**The direction is not a departure from the design; it is the design.** The foundation document
-already states the commitment:
-
-> The dependency is on the **protocol**, not the product: AMQP for the bus, S3 for the object
-> store, the OCI protocol for the registry. That is what keeps the naming safe rather than a
-> commitment that cannot be revisited.
-
-The object store is also **not** a foundation service — ADR 0028 removed it, and it is an
-ordinary module required through the module graph by whatever wants one. (The "exception that is
-not a swap" in that passage is the relational store, whose provisioning model borrows PostgreSQL's
-own meaning of databases, roles and schemas. The object store carries no such coupling: a bucket
-is a bucket.) So this effort is an instantiation of an existing principle, not a redesign — which
-is the cheapest kind of decision to make and the strongest kind to cite.
-
-## What the replacement has to carry, measured
-
-Taken from the module's manifest, its composition, its tool surface, and a search for its
-consumers across the catalogue — not from assumption.
-
-| Requirement | Evidence in the module today |
-|---|---|
-| S3 API | The protocol every consumer speaks; already the design's stated dependency. |
-| ~~OIDC login against the mesh's identity provider~~ | **Struck 2026-09-24. Not a requirement, and it never worked.** Six variables are wired and an entrypoint blocks on the provider, which reads as a live feature. The module's own hook comment records the end state as *"policy claim missing"* — a failing login. See [01](01-candidate-comparison.md). |
-| **Per-application access keys, each scoped to a bucket** | The real requirement. A "user" of the store is normally an application; the mesh already mints a credential per provisioned bucket. |
-| **One live consumer using it as opaque primary storage** | A file-sync application, since early 2023: objects named by internal id, metadata in its own database. Highest-risk consumer — a live copy drifts, and its bucket name must be preserved. |
-| Erasure-coded multi-node topology | Four server nodes with two data directories each, behind a load balancer. |
-| A single-node form | Declared as a flavour, for development and small nodes. |
-| Buckets as a typed provision | The module declares a provision type of `bucket` on a named network; the mesh mints the credential and the provider creates it (ADRs 0048, 0084). |
-| A tool surface | Bucket create/list/delete, object list/info/delete, presigned URL, and provisioning. |
-| A console | Published on its own subdomain through the reverse proxy, with an unlimited request-body middleware for uploads. |
-
-**Consumers, counted:** one application module, one capture module that takes a private bucket per
-node, one workflow module's tools, and the delivery/rescue internals of the shared library. The
-surface is small — the cost is concentrated in the provisioning handler, the tool handlers and the
-OIDC story, not spread across the catalogue.
-
-## Candidates
-
-**Four candidates, not three.** The comparison was briefly narrowed to SeaweedFS on the strength of
-console single sign-on; that axis turned out not to be a requirement, and the incumbent's own
-maintained fork had been omitted altogether. Both errors, and why they happened, are recorded in
-[01 — the candidates measured](01-candidate-comparison.md), which carries the evidence and the
-requirement-by-requirement detail.
-
-In short, and only in short:
-
-- **The maintained fork of the incumbent** — the community edition was archived and its images
- deleted, but a fork publishes, tracks CVEs, and preserves the on-disk format, S3 API and
- environment surface. Costs **an image reference** where every other option costs a data
- migration, two rewrites and a maintenance window. Does not end the dependence on an abandoned
- codebase; buys time to choose deliberately.
-- **Garage** — its permission model *is* the requirement (per access key, per bucket), its admin
- API is the closest match to how the mesh provisions, and the highest-risk consumer is
- first-party documented against it. Remaining cost: no object versioning, no server-side
- encryption or object locking, partial lifecycle — **unmeasured against the ten buckets, and the
- one thing that could still disqualify it**.
-- **SeaweedFS** — longest field record and erasure coding. Its console sign-on is a paid feature,
- which is now beside the point. What weighs against it is narrower: its S3 surface is a gateway
- translating onto its own file-system API, with no first-party support for the opaque consumer.
-- **RustFS** — closest in shape to the incumbent, so the least porting. But it reached general
- availability eight days before this was written, and carries an open defect in the credential
- path. Two earlier claims about it are corrected in 01: it is **not** a drop-in that retains
- existing data.
-- **Ceph RGW** — remains rejected as disproportionate where the object store is an ordinary
- module rather than a platform.
-
-**This is now two decisions, not one:** whether to repoint to the fork or migrate, and — if
-migrating — to which. Repointing does not foreclose migrating, which is the argument for taking it
-first. On the corrected requirement the migration ranking is Garage, then SeaweedFS, and not yet
-RustFS. Two measurements gate any graduation: **which S3 endpoints the consumers actually call**
-(Garage cannot be ranked fairly until counted), and **whether the fork can read the incumbent's
-on-disk format in place** — tested on a copy, because the migration between them is one-way. Both
-are in [01](01-candidate-comparison.md#what-is-still-unmeasured).
-
-## The migration track, in outline
-
-Data movement is the easy half, and deliberately reversible.
-
-1. **Stand the replacement up beside the incumbent**, on its own ports, its own provision type and
- **its own data directory**. Nothing removed. The data directory matters: reusing one the
- incumbent already holds would put a fresh single-drive store on top of a live erasure set.
-2. **Copy bucket by bucket with a neutral tool.** `rclone` rather than the incumbent's own client
- — the client has been withdrawn upstream too, so building the migration on it would inherit
- the same dependency this effort exists to remove.
-3. **Verify per bucket** — object counts and checksums, not a transfer exit code.
-4. **Repoint consumers through the connection the module already publishes.** Consumers read an
- API URL from the module's declared connections rather than addressing the store directly, so
- the cutover surface is that value plus the provisioning and tool handlers.
-5. **Freeze writes, final incremental sync, flip**, and keep the incumbent read-only as the
- rollback until confidence is earned. For the opaque consumer this is **not optional and not
- instant**: it stores objects by internal id with metadata in its own database, so a copy taken
- while it runs will drift. It needs a maintenance window for the final sync, and the window is
- proportional to 82,496 objects rather than to 230 GiB.
-6. **Retire**, and only then remove the module.
-
-The genuinely new work is not the copy. It is the **provisioning handler** and the **tool
-handlers**, both written against the incumbent's admin API. *The OIDC wiring was previously listed
-here and is struck: it is not a requirement and it never worked.*
-
-## Open questions
-
-- ~~How much of the OIDC requirement survives, and in which build?~~ **Answered, and it was the
- wrong question.** The console requirement does not exist, and the login it referred to never
- worked. What replaced it: which S3 endpoints consumers actually call, and whether the fork reads
- the incumbent's format in place.
-- Does the mesh's bucket provision translate to the candidate's identity model without weakening
- what ADR 0049 says about a consumer's identity fitting the tightest backend?
-- Should this effort also answer issue 113's general question — mirroring third-party images into
- the mesh's own registry — or is that a separate decision? Replacing one withdrawn product with
- another unmirrored upstream leaves the same one-way door in place, just further from the hinge.
-- Is the four-node erasure-coded topology still warranted, or was it inherited? Worth re-asking
- while the product is being chosen, rather than reproducing a shape by default.
diff --git a/01-RESEARCH/015-the-object-store-after-minio/01-candidate-comparison.md b/01-RESEARCH/015-the-object-store-after-minio/01-candidate-comparison.md
deleted file mode 100644
index d5fba1d6..00000000
--- a/01-RESEARCH/015-the-object-store-after-minio/01-candidate-comparison.md
+++ /dev/null
@@ -1,176 +0,0 @@
-# 015 / 01 — The candidates measured
-
-*Rewritten 2026-09-24. An earlier version of this document ranked the candidates on whether they
-preserved single-sign-on to the object store's **console**. That was the wrong axis — it is not a
-requirement — and a fourth candidate was missing entirely. Both errors are recorded at the end,
-because how a comparison came to be ranked on the wrong thing is worth more than the ranking was.*
-
-## The requirement, corrected
-
-Taken from the operator and from the running system, not from the module's shape.
-
-**A "user" of the object store is normally an application.** The requirement is therefore
-**per-application access keys, each scoped to its own bucket** — not per-human single sign-on. The
-mesh already works this way: it mints a credential for every provisioned bucket, and the consumer
-reads an endpoint from the module's declared connection rather than addressing the store directly.
-
-**The console is not a requirement.** It was the axis the previous version ranked on, and it should
-not have been.
-
-**The identity-provider login never worked.** The predecessor's module wires six OIDC variables and
-blocks startup until the provider answers, which reads like a working feature. It is not: the
-module's own hook comment records the end state as *"policy claim missing"* — a **failing** login,
-written up as progress because it proved the provider had registered. The identity provider emits no
-such claim, nothing in the module creates the mapper, and the configured scope alone would not carry
-a custom one. Two days of logs show no genuine login attempts, only internet scanners failing on an
-STS API version. **Nothing should be carried forward on the assumption this works**, and no
-candidate should be credited or penalised for matching it.
-
-**One consumer is live, opaque, and holds real user files.** A file-sync application has used the
-store as its **primary storage** since early 2023: objects named by an internal id, with all
-metadata in its own database. Three consequences — a copy taken while it runs will drift, its bucket
-name must be preserved or its database references break, and it is the highest-risk consumer of the
-lot.
-
-## What is actually stored, measured
-
-| | |
-|---|---|
-| Logical | **230 GiB, 82,496 objects, 10 buckets** |
-| Raw on disk | **468 GiB** — eight drive directories at 59 GiB each |
-| Implied scheme | 468 ÷ 230 = **2.03×**, confirming erasure coding at half parity |
-| Headroom | ~1.3 TiB free on the filesystem holding it |
-
-**All eight "drives" are directories on one filesystem on one machine.** The erasure coding is
-therefore not buying independent-drive redundancy; the real failure domain is the array underneath,
-which has its own. This single fact decides more of the comparison than any product feature: a
-scheme's redundancy model is close to irrelevant here, and what remains is its storage overhead.
-
-At 230 GiB with 1.3 TiB free, **storage overhead is not a deciding cost either.** Replication at
-three copies would run ~690 GiB against the present 468 GiB — about **+222 GiB**, comfortably
-absorbed. Erasure coding at a wider stripe would *save* roughly 146 GiB. Both are rounding errors
-against the headroom, and neither should decide this.
-
-## The candidates
-
-Four, not three. The previous version omitted the first.
-
-### The maintained fork of the incumbent
-
-The community edition was archived upstream and its images deleted
-([issue 113](../../04-ISSUES/113-the-object-stores-images-were-withdrawn-upstream/00-report.md)),
-but **a fork is maintained and publishing** — `pgsty/minio`, from the Pigsty project. It restores
-the console stripped from the community
-build, rebuilt image and package distribution, tracks CVEs, and states that it preserves the on-disk
-format, the S3 API and the environment-variable surface. Verified by pulling it: it reports a
-current release, permissive-to-copyleft licensing unchanged from upstream, and identifies itself as
-a community fork. Adoption is real — the server image has been pulled three quarters of a million
-times.
-
-**Why it reorders the comparison.** Every other candidate costs a data migration, a provisioning
-handler rewritten against a different admin API, a tool surface ported, and a maintenance window for
-the opaque consumer. The fork costs **an image reference**. It also closes the issue's one-way door:
-a node holding nothing can provision the module again, and patches resume.
-
-**What it does not do** is end the dependence on a codebase its original authors abandoned. It is
-maintenance mode, largely one project's effort, with no new features intended. It buys time to
-choose deliberately rather than under pressure — which is worth a great deal, and is not the same as
-a decision.
-
-### Garage
-
-**The best fit for how the mesh provisions.** Its permission model is *per access key, per bucket,
-read/write/owner* — which is the requirement above stated verbatim rather than approximated. Its
-admin API is a first-class REST surface with tokens scopeable to exactly the two operations a bucket
-provision performs. The opaque consumer is **first-party documented** against it, for primary
-storage, including client-side encryption support.
-
-Its previously-recorded penalties mostly dissolve under the corrected requirement: it has no console
-and no identity-provider integration, neither of which is wanted; and it replaces AWS-style ACLs and
-bucket policies with its own per-key-per-bucket model, which is the thing being asked for.
-
-**What genuinely remains.** It replicates rather than erasure-codes — immaterial at this volume and
-on a single array, as above. It does **not implement the full span of S3 endpoints**: object
-versioning is absent, object locking and server-side encryption endpoints are absent, and lifecycle
-is partial. **Whether any of the ten buckets depends on those is unmeasured, and it is the one thing
-that could still disqualify it.**
-
-### SeaweedFS
-
-Longest field record of the group, permissive licence, erasure coding, and identity-provider
-integration on the S3 API through token exchange. **Its console sign-on is a paid feature** — the
-admin UI itself is open, its identity integration is not. That finding is what falsified the
-previous version's narrowing, and it is now largely beside the point, since the console is not a
-requirement.
-
-What weighs against it here is narrower and more specific: its S3 surface is a **gateway
-translating onto its own file-system API**, with acknowledged divergence from AWS behaviour at the
-edges, and there is no first-party documentation for the opaque consumer. For a store already
-holding real user files in an opaque layout, first-party support is worth more than a feature list.
-It also carries more moving parts than a single-machine deployment needs.
-
-### RustFS
-
-Closest in shape to the incumbent — a similar admin API and client compatibility, so the existing
-handlers would port with least effort — under a permissive licence, with erasure coding and a
-console that does integrate an identity provider.
-
-Two things were recorded about it earlier that were **wrong, and are corrected here**: it is *not* a
-binary-level drop-in that retains existing data (API compatibility and on-disk compatibility are
-separate paths, and the on-disk one is preview-scoped with documented encryption limits), and it
-therefore offers no shortcut around the migration. It also carries **an open defect in the exact
-area the mesh depends on** — an access key created by an identity-provider user reported denied on
-all S3 operations.
-
-Decisively for now: **it reached general availability eight days before this was written.** For a
-component holding 230 GiB of real user files, field record is a feature, and it does not have one.
-
-### Ceph RGW
-
-Remains rejected, for the reason already recorded: disproportionate where the object store is an
-ordinary module rather than a platform.
-
-## Where this leaves it
-
-**The decision is no longer "which product replaces the incumbent".** It is two decisions, and they
-can be taken in either order but should not be confused:
-
-1. **Repoint to the maintained fork, or migrate now?** Repointing is an image reference and it
- closes the issue. Migrating now costs a data copy, two rewrites and a maintenance window, and
- buys independence from an abandoned codebase sooner.
-2. **If migrating, which?** On the corrected requirement the ranking is **Garage first** — its
- permission model *is* the requirement, its provisioning API is the closest match, and the
- highest-risk consumer is first-party supported. SeaweedFS second, on field record, with a
- translation-layer caveat that matters more here than its feature list. RustFS not yet, on age.
-
-Taking (1) does not foreclose (2), and that asymmetry is the argument for taking (1) first.
-
-## What is still unmeasured
-
-1. **Whether any of the ten buckets needs object versioning, server-side encryption or lifecycle.**
- This gates Garage specifically and nothing else here answers it.
-2. **Whether the fork's release can actually read the incumbent's on-disk format in place.** The
- format is claimed compatible across a multi-year gap; the migration between them is one-way, so
- this is tested on a copy or not at all.
-3. **Whether the opaque consumer's maintenance window is acceptable**, and how long it actually is
- at 82,496 objects.
-4. **Whether the eight-drive erasure-coded shape is warranted at all.** The evidence above says it
- is not buying what it appears to: eight directories, one array, one machine. It looks inherited.
-
-**Nothing graduates to a decision before 1 and 2.**
-
-## Two errors in the previous version of this document
-
-Recorded because the shape of both survives anonymisation and neither is unique to this effort.
-
-**It ranked on a requirement that did not exist.** Console single sign-on was treated as the axis
-because the module's configuration showed it wired up, and a wired-up configuration was read as a
-used feature. It was neither used nor working. *A configured feature is not an observed one*, and
-the evidence needed was the operator's answer and the logs — both cheap, neither consulted before
-the ranking was written.
-
-**It omitted the incumbent's own fork.** The whole effort began because an upstream withdrew its
-images; whether anyone had continued that upstream was the first question to ask and it was not
-asked. The candidate list was assembled from a search for *alternatives*, which by construction
-returns things that are not the incumbent. *When a dependency dies, "who took it over" precedes
-"what replaces it".*
diff --git a/01-RESEARCH/016-how-a-credential-can-be-rotated/00-overview.md b/01-RESEARCH/016-how-a-credential-can-be-rotated/00-overview.md
deleted file mode 100644
index 49eabbc0..00000000
--- a/01-RESEARCH/016-how-a-credential-can-be-rotated/00-overview.md
+++ /dev/null
@@ -1,57 +0,0 @@
----
-status: graduated
-became:
- - 02-DECISIONS/0114-a-shared-credential-rotates-over-two-credentials.md
- - 03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md
-initiated: 2026-09-26
-touches:
- - 02-DECISIONS/0113-the-vault-makes-every-secret.md
- - 02-DECISIONS/0049-a-consumers-identity-fits-the-tightest-backend.md
- - 02-DECISIONS/0048-a-provider-creates-the-credential-the-mesh-minted.md
- - 03-DESIGN/01-to-be/13-credentials-and-their-rotation.md
- - 03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md
- - 04-ISSUES/103-a-container-is-not-recreated-when-a-file-it-reads-changes/00-report.md
----
-
-# 016 — How a credential can be rotated
-
-**What.** Which rotation mechanisms the mesh's providers can actually support, measured against
-their code rather than assumed. Every provider in the catalogue was read, found by listing every definition that provides something: how it names what it
-makes for a consumer, what its remove destroys, whether it re-applies a password, whether its
-backend can hold two secrets for one login or two logins on one resource, and how its own
-administrative credential is set. The consumer side was read too: when a module reads a secret, and
-what makes it read a new one.
-
-**Why.** [ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md), as first drafted,
-chose *overlap*: add a second login beside the first, move every reader, then remove the old one,
-"through the adapter's existing create and remove", with "no consumer changes". A review showed that
-claim false. In most providers the consumer's data is named after its login, and remove drops the data
-with the login. Overlap as written would have deleted every consumer's database on its first
-rotation. The mechanism has to be chosen on what the providers do.
-
-**What it touches.** Rotation in 0113 and [to-be 27](../../03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md),
-which [ADR 0114](../../02-DECISIONS/0114-a-shared-credential-rotates-over-two-credentials.md) decided on
-these findings. The identity budget in
-[ADR 0049](../../02-DECISIONS/0049-a-consumers-identity-fits-the-tightest-backend.md), if a consumer
-gets two logins. The rotation already implemented, which [to-be 13](../../03-DESIGN/01-to-be/13-credentials-and-their-rotation.md)
-describes.
-
-**Documents.**
-
-- [01 — The providers](01-the-providers.md): the survey, one row per provider, and what it shows.
-- [02 — The readers](02-the-readers.md): how a secret reaches a running process, and what already
- recreates it.
-- [03 — The options](03-the-options.md): each rotation mechanism against those facts, and a
- recommendation.
-
-**Finding, in one paragraph.** All nine credential providers already re-apply a consumer's password
-in place on every create, and the controller's `rotate` command relies on that. It is a working
-rotation with a stated window. Eight of the nine name the consumer's resource after its login, and five
-destroy the consumer's data when they remove the login. The harness, keyed by login, would do the same
-on any change of login. Only one backend holds two passwords on one login, and two more hold several
-tokens. Eight backends can grant two logins the same rights over one resource; the ninth can give one
-login a second token. So every provider can hold **two credentials** over one resource, but only after
-each adapter separates *the consumer's resource* from *the credential that reaches it*. In postgres
-that also means the resource belongs to a role no login owns. Administrative credentials are a
-different case. They have one party and a fixed name, and five backends take them only at first
-initialisation, so changing one needs the old and the new value at once.
diff --git a/01-RESEARCH/016-how-a-credential-can-be-rotated/01-the-providers.md b/01-RESEARCH/016-how-a-credential-can-be-rotated/01-the-providers.md
deleted file mode 100644
index 8ce34f19..00000000
--- a/01-RESEARCH/016-how-a-credential-can-be-rotated/01-the-providers.md
+++ /dev/null
@@ -1,103 +0,0 @@
-# 01 — The providers
-
-Read from the catalogue's main branch: each provider's provisioner adapter (`create`, `remove`),
-the client functions they call, and each definition's own credentials. The providers were found by
-listing every definition that provides something and has a provisioner, not from memory. A first pass
-of this survey worked from memory and missed one, mailu.
-
-**The provisioner harness** in `mesh-sdk` calls `create` for a consumer when its contribution appears
-or changes (its login, password or values), and after the provisioner restarts. It calls `remove` for
-a login it applied earlier in the same process that is no longer contributed. Its record of what was
-applied is kept in memory and keyed by login. Two things follow:
-
-- a consumer whose derived login changes is removed under the old login and created under the new one,
- in one pass;
-- a contribution that disappears while the provisioner is down is never removed, and is left behind.
-
-## The credential providers
-
-`login` is the consumer's derived identity, which the adapter receives as `as`
-([ADR 0049](../../02-DECISIONS/0049-a-consumers-identity-fits-the-tightest-backend.md)).
-
-| provider | the consumer's resource is named | remove destroys | create re-applies the password | two secrets on one login | two logins on one resource |
-|---|---|---|---|---|---|
-| postgres | a database named `login`, owned by the role `login` | the database and the role | yes, `ALTER ROLE … PASSWORD` when the role exists | no: a role has one password | yes, but only through a role that cannot log in owning the database, with each login working as it. Otherwise whatever one login creates is its own, and dropping that login means handing its objects over first. Not done today |
-| mssql | a database named `login`, with the login mapped into it | the database and the login | yes, `ALTER LOGIN … WITH PASSWORD` | no: a login has one password | yes, two logins mapped to users in `db_owner`. A user owning a schema cannot be dropped, and a login with an open session cannot. Not done today |
-| mongodb | a database named `login`, with a user holding `dbOwner` | the database and the user | yes, `updateUser` with the new password | no: a user has one credential | yes, two users with `dbOwner` on one database. Not done today |
-| redis | the key prefix `login:` on an ACL user named `login` | the user, **not** its keys | yes: `ACL SETUSER … reset … >password` replaces all of them | **yes**: an ACL user holds several passwords, added with `>` and removed with `<`. Today's `reset` discards all but the new one | yes, two users on one key prefix, once the prefix is not the login |
-| minio | a bucket derived from `login`, and a service account whose access key is `login` | the access key; the bucket **only if empty**. A bucket holding objects is left, and the failure logged | yes, by removing the access key and adding it again, which leaves a moment with no key | no, but an access key *is* the login: a second key is a second login | yes, two service accounts with one bucket policy. The access key is capped at 20 characters |
-| lavinmq | a virtual host named `login`, and a user named `login` with permissions on it | the virtual host, with any queued messages, and the user | yes, the user is written again with the password | no: a user has one password | yes, permissions for two users on one virtual host |
-| mosquitto | a client named `login`, with a role named for it on the topic prefix `login/#` | the client and its role | yes, the password is set when the client exists | no: a client has one password | yes, two clients holding one role, once the prefix is not the login. The MQTT client identifier is chosen by the consumer, not tied to the login; a duplicate one takes the older session over |
-| mailu | a mailbox `login@domain`, unless the consumer contributes its own account name | the mailbox with its mail, for a login-named one; a contributed name is left for an operator | yes, the password is set when the user exists | no for the password; a user can hold several authentication tokens, per the backend's documentation | **no**: a mail user *is* its mailbox |
-| gitea (npm) | a user named `login` on a team of an organisation that owns every package | the user; **packages survive**, because the organisation owns them | yes, the user's password is set on every run | no for the password; a user can hold several access tokens | yes, trivially: a second member of the same team |
-
-## The other providers
-
-| provider | answers with | credential |
-|---|---|---|
-| umami | a website, found by its public name | none. The site id it makes has no way back to the consumer today |
-| cloudflare-dns | a public name derived from `login` | none handed to the consumer; its own API token is an operator value |
-| showcase | a route | none |
-| mesh-vault | custody: it records and withdraws sealed values in a ledger | it holds secrets; it makes none today |
-
-verdaccio provides the npm registry too, and has no provisioner.
-
-## Each provider's own administrative credential
-
-| provider | identity | how the backend takes it |
-|---|---|---|
-| postgres | a fixed superuser | from a file **only at first initialisation** |
-| mssql | `sa` | from the environment at first setup. The image documents no file form, and the definition records that as a declared exception |
-| mongodb | a fixed `root` | from a file **only at first initialisation**, when the data directory is empty |
-| mosquitto | a fixed admin client | seeded into the broker's dynamic-security file **once**; the seeding step skips when the file exists |
-| lavinmq | a fixed admin name | per its own bootstrap code, **only on a first boot** with an empty data directory. No resource in the definition runs that bootstrap; what sets it on a running mesh is outside the catalogue |
-| redis | the default user | from `requirepass` in a configuration the mesh renders, read when the server starts |
-| minio | a fixed root user | from a file, read when the server starts |
-
-**In five of seven, a new administrative value takes effect only through a command run with the old
-one.** The credential file is mounted directly into both the server and the provisioner. So replacing
-it recreates the provisioner, which then holds only the new value while the backend still expects the
-old one, and the provisioner is locked out. That is worse than changing nothing.
-
-Every provider module also has its own bus account, an own secret, read at start.
-
-## What the tables show
-
-1. **Every credential provider already rotates in place.** All nine re-apply the password on the
- same login each time `create` runs. The controller's `rotate` command relies on that: it replaces
- the credential in the inventory and sends both ends in one push. Its own comments state the window,
- between the provider applying and the consumer restarting, in which the consumer cannot
- authenticate.
-2. **Eight of nine name the consumer's resource after its login.** Only gitea separates them,
- because an organisation owns the packages. A second login therefore has no resource of its own to
- reach, and cannot share the first one's without the adapter granting it.
-3. **Five of nine destroy the consumer's data when they remove the login**: postgres, mssql and
- mongodb drop the database, lavinmq drops the virtual host with its queued messages, and mailu
- deletes the mailbox with its mail. minio drops only an empty bucket, and redis leaves the keys. In
- those five, *retire a login* and *delete the consumer's data* are one call. With the harness keyed
- by login, a changed login triggers it too.
-4. **One backend holds two passwords on one login** (redis). Two hold several tokens beside one
- password (gitea and mailu). A rotation built on two secrets per login would work for three
- providers out of nine.
-5. **Eight of nine can give two logins the same rights over one resource.** Group roles in postgres,
- database roles in mssql and mongodb, permissions in lavinmq, a shared role in mosquitto, a shared
- policy in minio, a shared key prefix in redis, a shared team in gitea. mailu cannot, because its
- user is its mailbox, but it can give one user a second token. So every provider can hold **two
- credentials** over one resource, though not every one as two logins. No adapter does either today.
-6. **Ownership is a trap in two backends.** In postgres whatever a login creates is that login's, so a
- second login cannot alter the first one's tables, and the first cannot be dropped while it owns
- them. The one-step way out deletes them. In mssql, a login cannot be dropped with a session open,
- nor its user while it owns a schema.
-7. **The administrative credentials have one party and a fixed name**, and five backends take them
- only at first initialisation. The provisioner needs the old and the new value at once to change
- them. Today nothing can give it both.
-8. **A consumer's identity is already the resource's name.** The login is derived from the
- assignment, which is a module on a node, so the current login and "the consumer" are the same
- string today. A second login would need a new name. The resource can keep the one it has.
-
-## Seen on the way
-
-The redis configuration names no ACL file, so a consumer's ACL user exists only in memory. A restart
-of the redis server erases every consumer's user. The provisioner does not create them again until it
-restarts itself, because its in-memory record says they are done. That is not a rotation finding, but
-it is a live fault, and it is recorded here so it is not lost.
diff --git a/01-RESEARCH/016-how-a-credential-can-be-rotated/02-the-readers.md b/01-RESEARCH/016-how-a-credential-can-be-rotated/02-the-readers.md
deleted file mode 100644
index 143f53d5..00000000
--- a/01-RESEARCH/016-how-a-credential-can-be-rotated/02-the-readers.md
+++ /dev/null
@@ -1,46 +0,0 @@
-# 02 — The readers
-
-How a secret reaches a running process, and what makes the process take a new one.
-
-## No module watches a secret
-
-A search of every module's code in the catalogue found no file watching of any kind, and no
-re-reading of a secret while running. **Every reader reads a secret when it starts.** There is no
-consumer that takes a new value live, so every rotation that changes what a consumer presents ends in
-the consumer restarting.
-
-## The host already recreates what read a changed file
-
-The node host records, for every long-running container, the digest of each file it read when it
-was created: its env-files, and every file bind-mounted into it directly. When a digest changes, the
-host recreates the container, even though its spec is otherwise unchanged. This is the fix for
-[issue 103](../../04-ISSUES/103-a-container-is-not-recreated-when-a-file-it-reads-changes/00-report.md).
-It is on the host's main branch, while the issue is still recorded as located, not fixed.
-
-Two cases are deliberately left out and need `restart-on` in the definition:
-
-- a file read out of a **mounted directory**, because the host cannot know whether the service reads
- it once or watches it (a route proxy re-reads its routes live; a provisioner polls what it receives);
-- a **process** rather than a container.
-
-## What the catalogue does with it
-
-22 definitions declare a secret they receive. In 16 of them it reaches the service through a
-rendered file, usually an env-file. That case the host already covers. 14 declare `restart-on` for
-something. Whether each of the 22 is fully covered depends on how its secret travels: through an
-env-file or a direct mount, which the host covers, or through a directory or into a process, which
-needs `restart-on`. **That was not classified module by module.** It is the check to run before a
-rotation mechanism relies on it.
-
-The count covers only the `secrets` field. The 49 modules with their own bus account, and 54 with any
-own secret, are readers too, and their bus accounts are rotated like any credential two parties hold.
-Their files are mounted directly, which the host covers, but the classification has to name them.
-
-## What this means for rotation
-
-- The *read at start* half of 0113's recipient model is already true, and mostly already handled by
- the host. The restart is derived from the files a container reads, not declared per secret.
-- Any mechanism, in place or overlapping, ends with the reader being recreated. What differs is
- whether the credential it held until then still works.
-- For a single-party secret, a module's own, the reader is also the only holder. There is nobody to
- overlap with, and delivering the new file recreates the reader.
diff --git a/01-RESEARCH/016-how-a-credential-can-be-rotated/03-the-options.md b/01-RESEARCH/016-how-a-credential-can-be-rotated/03-the-options.md
deleted file mode 100644
index 5bfb7e53..00000000
--- a/01-RESEARCH/016-how-a-credential-can-be-rotated/03-the-options.md
+++ /dev/null
@@ -1,79 +0,0 @@
-# 03 — The options
-
-Three mechanisms, weighed against [01](01-the-providers.md) and [02](02-the-readers.md).
-
-## A. In place, as today
-
-The vault makes a new value. Every applier re-applies it on the same login, which all nine
-providers already do. Every reader is recreated by the host.
-
-- **Works with:** every provider, unchanged. It is what `rotate` does now.
-- **Costs:** a window per consumer, from the provider applying to the consumer being recreated. They
- are on different machines, and nothing orders them. A reader whose machine is unreachable from the
- mesh but still reaches its provider stays locked out until the mesh reaches it again.
-- **Admin credentials:** the natural form. The provider module is the only party, and it has to
- apply the new value with the old one anyway (finding 6).
-
-## B. Two secrets on one login
-
-The applier adds the new password beside the old one, readers move, and the old one is removed.
-
-- **Works with:** redis natively, and gitea and mailu through tokens. **Not** with the other six, whose
- backends hold one password per login (finding 4).
-- **Verdict:** not a mechanism, a special case. Using it where it exists and something else
- elsewhere is the "this way or that way" the design is trying to remove.
-
-## C. Two credentials per consumer, over one resource
-
-The consumer has two credentials and uses one at a time. The applier ensures the other with the new
-value and gives it the same rights over the consumer's resource. Readers move to it, and then the old
-credential is retired, which removes the credential only, never the resource. **What a credential is,
-is the adapter's**: a second login for eight providers (finding 5), a second token on the same login
-for mailu. The mesh sees one mechanism.
-
-- **Works with:** every provider, **after** each adapter changes:
- - the resource is named after the consumer, not the login. Today the two are the same string (finding
- 8), so existing resources keep their names, and the current login stays one of the two;
- - the resource is owned by the resource, not by a login. In postgres that is a role no one logs in
- as, which each login works as, and ownership of an existing database moves to it once (finding 6);
- - both credentials get the same rights, over data and structure;
- - *retire a credential* and *remove the consumer* become two operations. Today they are one call, and
- in five providers that call destroys data (finding 3). The harness must key by consumer, so that a
- changed login is not a removal. This is the whole of the danger, and it has to be split, whatever
- else is chosen.
-- **Costs:**
- - every credential adapter changes;
- - the harness learns the alternation and a confirmation per step, and rotation state has to live
- somewhere that survives a restart, which the harness's memory does not;
- - the second login's name must fit the tightest backend. That is 20 characters for a minio access
- key ([ADR 0049](../../02-DECISIONS/0049-a-consumers-identity-fits-the-tightest-backend.md)), and
- a suffix spends part of it;
- - retiring an mssql login has to end its sessions first.
-- **Gains:** no window. A reader that cannot be reached keeps a working credential until it can.
-- **Does not apply to** single-party secrets: admin credentials and a module's own secrets. There is
- no second party to overlap with.
-
-## Independent of the choice
-
-- **Split remove, and key the harness by consumer.** Retiring a credential, or a login changing, must
- never be able to destroy a consumer's data. That holds under A too, because A's remove is the same
- call.
-- **Admin credentials are applied by their own provider**, using the old value, with the new one staged
- beside it. Five backends take the value only at first initialisation. Replacing the file first locks
- the provisioner out (finding 7).
-- **Classify the readers** ([02](02-the-readers.md)), bus-account readers included, before relying on
- derived restarts.
-
-## Recommendation
-
-- **Two-party credentials, consumer credentials and bus accounts: C**, because it is the only
- mechanism every provider supports, and it closes the window instead of shortening it. Its prerequisite, separating the resource from the login and
- retiring a login from removing a consumer, is worth doing on its own, because it removes a
- data-loss path that exists today.
-- **Single-party secrets (admin credentials, a module's own): A, staged.** In place, applied by the
- provider that holds them, with the new value beside the old until it has taken.
-- **Until the adapters are changed, A stays** as `rotate` implements it, with its window stated. It is
- not replaced by a mechanism the providers cannot yet carry.
-
-This is two mechanisms, split by a property of the secret rather than by provider: whether it has one
-party or two. Every provider is treated the same way for the same kind of secret.
diff --git a/01-RESEARCH/017-a-mesh-that-heals-itself/00-overview.md b/01-RESEARCH/017-a-mesh-that-heals-itself/00-overview.md
deleted file mode 100644
index 9ecced2f..00000000
--- a/01-RESEARCH/017-a-mesh-that-heals-itself/00-overview.md
+++ /dev/null
@@ -1,70 +0,0 @@
----
-status: graduated
-initiated: 2026-09-26
-touches:
- - 00-META/mission.md
- - 02-DECISIONS/0106-the-bus-is-nats.md
- - 02-DECISIONS/0010-delivery.md
- - 02-DECISIONS/0083-one-push-leaves-the-mesh-consistent.md
- - 03-DESIGN/01-to-be/06-the-controller.md
- - 03-DESIGN/01-to-be/09-the-node-lifecycle.md
- - 03-DESIGN/00-as-is/09-interfaces-and-observability.md
-became:
- - 02-DECISIONS/0116-the-bus-is-built-in-five-steps.md
- - 02-DECISIONS/0224-a-provider-that-keeps-failing-a-consumer-is-a-problem-the-controller-reports.md
- - 02-DECISIONS/0227-the-core-holds-nine-rules-each-checked-and-is-built-to-them-in-six-phases.md
- - 02-DECISIONS/0231-a-healer-acts-on-what-observation-raised-and-only-observation-says-it-worked.md
- - 02-DECISIONS/0234-the-mesh-holds-a-conversation-with-its-operator.md
- - 02-DECISIONS/0240-a-module-says-how-it-is-healthy-and-the-node-engine-judges-it.md
- - 02-DECISIONS/0253-a-condition-says-itself-to-the-operator-in-plain-words-beside-its-summary.md
- - 03-DESIGN/01-to-be/25-the-bus-on-nats.md
- - 03-DESIGN/01-to-be/28-building-the-bus.md
- - 03-DESIGN/01-to-be/45-a-core-that-cannot-fail-silently.md
----
-
-# 017 — A mesh that heals itself
-
-**What.** The behaviour the operator wants: a mesh that runs itself. It notices what is wrong,
-repairs what it can, and hands what it cannot repair to someone who can, with the reason. This effort
-writes that wish down as intended behaviour, designed for the bus the mesh is moving to
-([ADR 0106](../../02-DECISIONS/0106-the-bus-is-nats.md): NATS). It also records what can be done
-pragmatically before that move.
-
-**Why.** The mission is *a mesh that controls itself* ([mission](../../00-META/mission.md)). The
-mesh can tell whether it is up. It cannot tell whether it is right. The as-is page on observability says so
-([as-is 09](../../03-DESIGN/00-as-is/09-interfaces-and-observability.md)). To-be 06 names an
-`observability` context in the controller and leaves its store undecided. Nothing routes a condition
-the mesh cannot fix to anyone. The cost is measurable: **46 of the 116 issue reports in this
-repository describe a failure that was silent.** A mesh that heals itself is, first, a mesh that stops
-failing silently.
-
-**What it touches.** The controller's observability context, the node lifecycle's liveness, delivery
-([ADR 0010](../../02-DECISIONS/0010-delivery.md), [ADR 0083](../../02-DECISIONS/0083-one-push-leaves-the-mesh-consistent.md)),
-the provisioner harness, and rotation, which is proposed alongside to-be 27 as ADR 0114.
-
-**Documents.**
-
-- [01 — The intended behaviour](01-the-intended-behaviour.md): the wish, as principles and as how
- the mesh behaves once the bus is NATS.
-- [02 — Now, pragmatically](02-now-pragmatically.md): what is done before NATS, why it does not
- build anything the move would throw away, and what has been done already.
-
-**Next.** Two measurements this effort owes before it can graduate:
-
-1. **Every loop in the mesh**: what it converges, and whether it compares against observed state or
- against its own memory. Issue 120 found the provisioner harness trusting memory. The same pattern is
- expected elsewhere.
-2. **The 46 silent failures, classified**: a missing observation, a loop trusting memory, or a missing
- escalation. That shows which mechanism removes the most of them.
-
-## What it became
-
-*Closed 2026-10-10.* The intended behaviour is decided and designed. [ADR 0227](../../02-DECISIONS/0227-the-core-holds-nine-rules-each-checked-and-is-built-to-them-in-six-phases.md) takes this effort's principles into the
-core's nine rules, designed in [to-be 45](../../03-DESIGN/01-to-be/45-a-core-that-cannot-fail-silently.md); a healer acts on what observation raised ([ADR 0231](../../02-DECISIONS/0231-a-healer-acts-on-what-observation-raised-and-only-observation-says-it-worked.md)); a
-provider that keeps failing a consumer is reported ([ADR 0224](../../02-DECISIONS/0224-a-provider-that-keeps-failing-a-consumer-is-a-problem-the-controller-reports.md)); a module says how it is healthy
-([ADR 0240](../../02-DECISIONS/0240-a-module-says-how-it-is-healthy-and-the-node-engine-judges-it.md)); the mesh holds a conversation with its operator ([ADR 0234](../../02-DECISIONS/0234-the-mesh-holds-a-conversation-with-its-operator.md), [ADR 0253](../../02-DECISIONS/0253-a-condition-says-itself-to-the-operator-in-plain-words-beside-its-summary.md)); the bus was built
-in steps ([ADR 0116](../../02-DECISIONS/0116-the-bus-is-built-in-five-steps.md), [to-be 25](../../03-DESIGN/01-to-be/25-the-bus-on-nats.md), [to-be 28](../../03-DESIGN/01-to-be/28-building-the-bus.md)). Of the two measurements this effort owed, the silent failures were classified by research 031,
-which this effort fed. The inventory of every loop was not made as a list; instead
-[ADR 0231](../../02-DECISIONS/0231-a-healer-acts-on-what-observation-raised-and-only-observation-says-it-worked.md) makes it the rule that a healer acts on what observation raised and only observation says
-it worked. One item is not decided: a condition that needs judgement, handed to an agent as
-work. It is tracked by issue 409.
diff --git a/01-RESEARCH/017-a-mesh-that-heals-itself/01-the-intended-behaviour.md b/01-RESEARCH/017-a-mesh-that-heals-itself/01-the-intended-behaviour.md
deleted file mode 100644
index 28385d70..00000000
--- a/01-RESEARCH/017-a-mesh-that-heals-itself/01-the-intended-behaviour.md
+++ /dev/null
@@ -1,85 +0,0 @@
-# 01 — The intended behaviour
-
-The operator's wish, written as behaviour: what a person or an agent sees the mesh doing. This is a
-target to design toward, not a design. Every part of it is to be decided through a record before it
-is built.
-
-## Principles
-
-**1. Every loop compares what should be with what is, never with what it did.** Desired state is the
-mesh's: assignments, requirements, seats. Observed state is read from the thing itself: the container,
-the backend, the node. A loop that compares against its own memory of what it applied is blind to
-anything that changed behind its back. That is issue 120, and it is the pattern this whole effort is
-written against.
-
-**2. Healing is the ordinary path run again, never a second path.** Repairing a lost login is
-provisioning it. Repairing a dead container is converging the node. Repairing a stale declaration is
-delivering it. A repair that needs its own code is a second way of doing something, which is exactly
-what the mesh is removing everywhere else.
-
-**3. A repair never destroys.** Healing may recreate, re-provision, re-deliver and restart. It may
-never delete a consumer's data, retire a credential someone still uses, or pick a winner between two
-contradictory states. Where the only repair is destructive, it is escalated.
-
-**4. Nothing fails silently.** Every condition the mesh cannot repair within its budget becomes
-visible. It is named, it says since when, why, and who can resolve it. It is visible until it is
-resolved, and resolved by observation, not by someone clicking it away.
-
-**5. What the mesh cannot fix goes to an agent.** Per the mission, an agent may be human or not. A
-condition that needs judgement is handed to one, as work, with what the mesh knows. It is not handed
-over as a notification that someone may or may not read.
-
-**6. Correctness, not only liveness.** A running process that authenticates with a dead credential,
-serves an old version, or routes nowhere is not healthy. What a provision's contract promises is what
-is checked: the credential authenticates, the route answers, the version is the declared one.
-
-## The loop, everywhere
-
-Every part of the mesh that owns something runs the same loop:
-
-1. **know** what should be true: from assignments, requirements and seats;
-2. **observe** what is true: from the thing itself, on its own cadence;
-3. **repair** the difference by running the ordinary path again, within a budget of attempts and
- time;
-4. **raise** a *condition* when the budget is spent or the only repair is destructive;
-5. **clear** the condition when observation shows it resolved.
-
-A **condition** is a durable fact about something the mesh owns, such as a node, an assignment, a
-provision, a seat or a rotation: what is wrong, since when, the evidence, what was tried, and who can
-resolve it. Conditions are the one thing a person or an agent looks at to know whether the mesh is
-right. `status` is the list of open conditions. When it is empty, the mesh is right, not just up.
-
-## On NATS
-
-[ADR 0106](../../02-DECISIONS/0106-the-bus-is-nats.md) moves the bus to NATS, and NATS makes most of
-this cheaper, because observation becomes something every component publishes rather than something
-a central process polls.
-
-| the wish needs | on NATS |
-|---|---|
-| every component says it is alive | a heartbeat on a subject per node and assignment; silence past its interval is a condition, and nobody polls |
-| every component says what it observed | observations published on subjects (`mesh.observed..`, for instance), consumed by whoever owns the comparison |
-| the last known state survives restarts | a JetStream key-value bucket of observed state per owner; the provisioner's "what I applied" and a rotation's step live there, not in memory |
-| conditions are durable and watchable | conditions as entries in a key-value bucket, watched by anyone who cares: a surface, an agent, the controller |
-| the bus itself is observed | the server's advisories (a consumer exceeding its deliveries, a slow consumer, a client disconnecting) and its monitoring endpoint become observations like any other |
-| a repair is retried, not lost | JetStream redelivery with delay, which is the same mechanism [ADR 0083](../../02-DECISIONS/0083-one-push-leaves-the-mesh-consistent.md)'s guarantee moves to |
-| work handed to an agent | a condition that needs judgement published as a task on a subject an agent's queue group consumes |
-
-**Who compares.** Each owner compares its own: the host for its node's containers and files, a
-provisioner for its backend, the vault for rotations, the controller for delivery and seats. The
-controller's observability context does not repair anything. It holds conditions, their history,
-and the view across the mesh. It notices what no owner can see about itself: an owner gone silent.
-
-## What stays human
-
-Some repairs need the operator's key, and the mesh says so rather than pretending otherwise:
-re-raising the vault or the broker, and recovering a node's identity. These are conditions too, with
-the procedure named, and they are the only ones that can never clear themselves.
-
-## Open
-
-- The budgets: how many attempts, over how long, per kind of repair.
-- How a condition that needs judgement reaches an agent, and how the agent's action is recorded.
-- Where the observability context stores history (to-be 06 left it open; volume argues against the
- relational store).
-- Which correctness probe each provision's contract offers, and how often it runs.
diff --git a/01-RESEARCH/017-a-mesh-that-heals-itself/02-now-pragmatically.md b/01-RESEARCH/017-a-mesh-that-heals-itself/02-now-pragmatically.md
deleted file mode 100644
index fe61353c..00000000
--- a/01-RESEARCH/017-a-mesh-that-heals-itself/02-now-pragmatically.md
+++ /dev/null
@@ -1,46 +0,0 @@
-# 02 — Now, pragmatically
-
-The intended behaviour lands on NATS. The bus moves after the migration's core
-([ADR 0106](../../02-DECISIONS/0106-the-bus-is-nats.md)). Until then, work toward it is chosen by one
-test:
-
-**Does it survive the move?** A change to what a loop compares against, or to what an adapter can
-tell about its backend, survives, because it is independent of the bus. A new AMQP queue for health
-reports, a poller written against the broker's management API, or a condition store built on the
-current broker does not survive, and is not built.
-
-## Done
-
-**The provisioner asks the backend, not memory** (issue 120). The SDK's harness gained an optional
-`holds` on the adapter, asked for every applied consumer every minute. A consumer the backend no
-longer holds is provisioned again. Being unable to ask is not treated as loss. The cache module
-implements it first, because its server keeps its users in memory and forgets them all on a restart.
-That was verified against a real server: a restart erases every consumer's user, and `holds` answers
-correctly for absent, present, wrong-password, disabled and deleted.
-Changes: mesh-sdk PR #7 (0.1.1) and mesh-catalog PR #84.
-
-This is principle 1 applied to one loop. It survives the move unchanged. On NATS, the harness's
-record of what it applied moves from memory into a key-value bucket, and `holds` stays as it is.
-
-## Next, in order of silent failures removed
-
-1. **`holds` for the other credential providers.** Each backend can answer whether a login exists
- with the mesh's password without changing anything. Where a backend cannot check a password without
- logging in, logging in is the check.
-2. **The harness's other blind spot.** A consumer that goes away while its provisioner is down is never
- removed. The fix is the same principle in reverse: list what the backend holds, and compare it with
- what the mesh asks for. Removal stays subject to ADR 0114's rule that it never follows from a login
- changing.
-3. **`status` reports what owners already know.** The host knows which containers it recreated and
- why. A rotation knows who it waits on. Delivery knows what is outstanding. Surfacing those as
- conditions in the existing `status` needs no new transport. It is the shape the NATS condition store
- will hold.
-4. **The loop inventory and the classification** in [00](00-overview.md). They decide what comes after
- these three.
-
-## Not now
-
-- Heartbeats, observation subjects, key-value state, advisories: all NATS, all after the move.
-- Handing conditions to agents: designed with NATS, where a task on a subject is native.
-- Choosing the observability store: decided when there is something to store, which is after the
- move.
diff --git a/01-RESEARCH/018-the-operators-machine-as-modules/00-overview.md b/01-RESEARCH/018-the-operators-machine-as-modules/00-overview.md
deleted file mode 100644
index 9b05c702..00000000
--- a/01-RESEARCH/018-the-operators-machine-as-modules/00-overview.md
+++ /dev/null
@@ -1,86 +0,0 @@
----
-status: graduated
-initiated: 2026-10-02
-touches:
- - 02-DECISIONS/0040-what-a-module-is.md
- - 02-DECISIONS/0011-managed-files-are-generated-never-edited.md
- - 02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md
- - 02-DECISIONS/0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md
- - 02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md
- - 02-DECISIONS/0161-what-deserves-a-seat.md
- - 03-DESIGN/01-to-be/05-the-node-host.md
- - 03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md
- - 03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md
- - 03-DESIGN/01-to-be/34-the-console.md
- - 03-DESIGN/00-as-is/10-module-catalogue.md
- - 04-ISSUES/160-a-machine-says-little-about-itself-and-only-when-asked/00-report.md
- - 04-ISSUES/168-a-setting-reaches-every-file-and-contribution/00-report.md
-became:
- - 03-DESIGN/01-to-be/37-the-operators-machine.md
- - 02-DECISIONS/0173-the-operators-machine-is-the-meshs-and-a-module-is-what-it-declares.md
- - 02-DECISIONS/0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md
- - 02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md
- - 02-DECISIONS/0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md
- - 02-DECISIONS/0177-a-unit-may-be-user-scoped-and-the-service-manager-is-a-node-seat.md
----
-
-# 018 — The operator's machine as modules
-
-**What.** The mesh owns the whole machine, not only the services on it. Everything a person
-configures on a node — the login manager, the display server, the window manager, the shell, the
-terminal, the launcher, the notifier, the audio setup, the boot images, the downloads folder, the
-agent at the terminal — is a module: a package, the files it owns under `/etc` and under the
-operator's home, the seat it holds, the tools it serves. One default configuration per module,
-varied per node only through settings rendered into the file or a kept operator region, never
-through an edit. The servers take the universal modules (shell, prompt, git, the agent); the
-workstations take those and the graphical stack, which a capability the machine reports gates.
-This effort writes that behaviour down, measures what the predecessor's desktop modules actually
-contain, and settles what the mesh must gain before the first of them can be written.
-
-**Why.** The predecessor is retired on every node. What it still owned on the two workstations —
-about thirty modules' worth of dotfiles, user units and `/etc` files — is now owned by nothing:
-no generator regenerates them, and a fix to one of them is a hand edit that nothing records. The
-migration scoped these modules out as *the workstation's own environment*, and
-[to-be 29](../../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md) names them as the last
-thing the predecessor was keeping alive. To-be 29 covers one directory, `~/.ssh`, and draws a
-boundary inside it. The operator wants no boundary: the machine is the mesh's, as far as it makes
-sense to configure it. That is a wider scope than any design states, and it reaches three records
-that were written for services: what a module is, where a module's tools run, and what a managed
-file may be.
-
-**What it touches.** The module definition ([ADR 0040](../../02-DECISIONS/0040-what-a-module-is.md)),
-seats and their contracts ([ADR 0132](../../02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md)),
-where a module's tools run ([ADR 0150](../../02-DECISIONS/0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md),
-[ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md),
-[to-be 33](../../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md) §6), the host's vocabulary
-([to-be 05](../../03-DESIGN/01-to-be/05-the-node-host.md)), managed files and settings
-([ADR 0011](../../02-DECISIONS/0011-managed-files-are-generated-never-edited.md),
-[issue 168](../../04-ISSUES/168-a-setting-reaches-every-file-and-contribution/00-report.md)),
-and the catalogue's shape ([as-is 10](../../03-DESIGN/00-as-is/10-module-catalogue.md)).
-
-**Documents.**
-
-- [01 — The intended behaviour](01-the-intended-behaviour.md): the operator's wish, written as
- how the mesh behaves, in the mesh's own words.
-- [02 — What exists, and what is missing](02-what-exists-and-what-is-missing.md): the
- predecessor's desktop modules measured; which records already say what is wanted; the gaps.
-- [03 — One tool executor per node](03-one-tool-executor-per-node.md): where a module's tools
- run. The direction the operator set, the evidence for it, and what it supersedes.
-- [04 — The seats of the environment](04-the-seats-of-the-environment.md): the roles a machine
- has once, their candidate contracts, and what gates each.
-
-**What it had to settle, and where each landed.** *(Graduated 2026-10-02.)*
-
-1. A module is one *managed thing*, software or not, and every module may serve tools — or ADR
- 0040 already says this and only its examples are narrow.
-2. One tool executor per node, host-side, module-agnostic; which records it supersedes and
- in what form the console continues.
-3. Per-node variation is a setting rendered into the file or a kept region, never an edit —
- ADR 0011 stands — and issue 168 is fixed before any environment module carries a setting.
-4. User-scoped units on the host's `service` shape, and a service-manager seat whose holder
- serves the tools about them.
-5. The operator account stated on every node; today no node record carries one.
-6. The seats of the environment and their verbs, one record per seat, slowly, because a
- seat's tools bind every future holder.
-7. Where the environment modules live: this catalogue, or one of their own as the media chain
- has; and whether a third-party organisation's tooling belongs in a public catalogue at all.
diff --git a/01-RESEARCH/018-the-operators-machine-as-modules/01-the-intended-behaviour.md b/01-RESEARCH/018-the-operators-machine-as-modules/01-the-intended-behaviour.md
deleted file mode 100644
index eb7cc96f..00000000
--- a/01-RESEARCH/018-the-operators-machine-as-modules/01-the-intended-behaviour.md
+++ /dev/null
@@ -1,99 +0,0 @@
-# 01 — The intended behaviour
-
-*Written 2026-10-02 from the operator's words, in the mesh's words. What is wanted, before what
-exists. Where a sentence restates a record, the record is named; where it goes further, that is
-said.*
-
-## The machine is the mesh's
-
-**Everything configurable on a node is declared by a module.** Not only the services the mesh
-runs: the login manager, the display server, the window manager, the bar, the launcher, the
-notifier, the compositor, the lock screen, the terminal emulator, the clipboard, the shell and its
-prompt, the editor, the audio setup, the boot images, the package manager's configuration, the
-agent a person runs at a terminal, and the folders a person works in — a downloads folder that is
-tidied, backed up, distributed to other nodes and asked questions of. System folders and the
-operator's home alike. The operator is the only person on every node, so the mesh manages the
-person's machine, not a machine with a person on it.
-
-This is [ADR 0040](../../02-DECISIONS/0040-what-a-module-is.md)'s definition applied without
-the service bias its examples carry. A module is one managed thing, named once, described
-completely by its manifest. It may have a package, files, a container, a unit, a binary, a seat it
-holds, and tools it serves — any one of these, or all, or two. There is **no kind of module**: zsh
-has a package, files, a seat claim and the tools that claim obliges it to serve; downloads has a
-folder, a process and tools; nftables has a package, files, a service, a seat and tools. The
-difference is what each declares, not what each is.
-
-**The home has no boundary.** [To-be 29](../../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md)
-owns one directory under the home and draws a line inside it between the mesh's and the person's.
-Here the line is drawn only by what the modules declare: every file some module places is the
-mesh's; what no module declares is found and left alone, exactly as the adoption rules already
-say for a machine. The reach is bounded by sense, not by a rule — the mesh configures what can
-be configured, and a person's documents, projects and history are data under
-[ADR 0051](../../02-DECISIONS/0051-shared-data-is-the-operators.md), not configuration.
-
-**A module names no node and no path.** The operator account is a node fact and the home is
-derived from it ([to-be 29](../../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md) §1–2,
-shipped in the controller; its record is proposed in an open change). A module places a file
-*under the home, owned by the account*, and the same manifest lands on a server and a laptop.
-
-## One default, varied by settings, never by edits
-
-**One module, one default configuration.** The window manager module ships the configuration
-that is right for every node. There are no flavors: the predecessor's one desktop module carried
-four, one per class of machine, and what differed between them is what settings are for.
-
-**A node varies a module in exactly two ways.** A **setting**, declared by the module with its
-type, meaning and default (proposed alongside the container-runtime records), set for the mesh
-or for one node, and rendered into the file at composition — the value is in the file, not in an
-environment variable the file reads. Or a **kept region**: a block in a file the mesh writes
-*into*, where the operator's own lines survive every push
-([ADR 0102](../../02-DECISIONS/0102-the-mesh-writes-into-a-shared-file-never-over-it.md)). An
-edit to a managed file outside such a region is not a third way; it is overwritten, as
-[ADR 0011](../../02-DECISIONS/0011-managed-files-are-generated-never-edited.md) says, and the
-predecessor's habit of adopting disk drift back into its database is not carried over.
-
-The predecessor's theming — some ninety environment variables substituted into templates at sync
-time, with tools to list and set them — is the same idea with the wrong rendering. The knobs
-become declared settings; the file carries the value.
-
-## Roles a machine has once are seats, and seats carry tools
-
-**A role a machine fills at most once is a node-scoped seat**, declared by a module
-([ADR 0121](../../02-DECISIONS/0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md),
-[ADR 0126](../../02-DECISIONS/0126-a-module-declares-its-own-seats.md)): the login shell, the
-display session, the display server, the terminal emulator, the launcher, the notifier, the
-compositor, the lock screen, the service manager, the boot loader. Several modules may be able to
-hold one — zsh, fish and bash can all hold the login shell — and the assignment on each node says
-which does. Installing a shell is installing software; holding the seat is being *the* shell.
-
-**A seat's contract is its tools** ([ADR 0132](../../02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md)).
-Every holder of the login-shell seat serves `execute`, which takes one string, the command, and
-runs it on the node the seat is scoped to. Every holder of the boot seat serves "rebuild the boot
-images", so *"rebuild your boot images"* is a verb addressed to a machine, not a one-off step in
-a hook. Every holder of the service-manager seat answers for the units on the machine, system and
-user scope. A module may serve its own tools beside the seat's
-([ADR 0170](../../02-DECISIONS/0170-the-firewall-seat-serves-its-verbs.md) §2): show the rendered
-configuration, set a theme value, report status.
-
-**Any tool may be called from any node.** The operator's statement, and the grant model it
-implies: the executor on each node may call everything, as the console already may. A verb that
-needs root on the machine is the module's concern — the tool escalates, the executor and the
-caller do not know.
-
-## Servers and workstations differ by capability, not by catalogue
-
-The same catalogue serves every node. A module declares what it needs — a graphical session, a
-display server, a container runtime — and the machine reports what it has, as the profile already
-reports eight capabilities today ([issue 160](../../04-ISSUES/160-a-machine-says-little-about-itself-and-only-when-asked/00-report.md)).
-Assignment refuses the wrong placement by name
-([ADR 0161](../../02-DECISIONS/0161-what-deserves-a-seat.md) §3). So every node takes the shell,
-the prompt, git and the agent; only a node with a graphical session can take the display server,
-and only a node holding the display server can take a window manager. Nothing in a module says
-"workstation".
-
-## What the operator would say to the mesh
-
-*Set the login shell on the build node to fish. Rebuild the laptop's boot images. Show me the
-window manager's effective configuration on the desktop and where each value comes from. Give
-the downloads folder on the laptop to the home server. Run `uptime` on every node.* Each of these
-is a seat verb or a module tool, addressed to a node, answered by whatever holds the role there.
diff --git a/01-RESEARCH/018-the-operators-machine-as-modules/02-what-exists-and-what-is-missing.md b/01-RESEARCH/018-the-operators-machine-as-modules/02-what-exists-and-what-is-missing.md
deleted file mode 100644
index c86083dc..00000000
--- a/01-RESEARCH/018-the-operators-machine-as-modules/02-what-exists-and-what-is-missing.md
+++ /dev/null
@@ -1,91 +0,0 @@
-# 02 — What exists, and what is missing
-
-*Measured 2026-10-02 on one installation: two workstations, two servers, all four converged to
-the mesh; the predecessor retired on the last workstation the day before. Numbers are from the
-machines and the repositories, not from memory.*
-
-## 1. What the predecessor's desktop looks like
-
-The predecessor's catalogue on the laptop held **34 modules**, of which **28** are the operator's
-environment rather than services. By what they declare:
-
-| shape | count | examples |
-|---|---|---|
-| package only | 9 | browser, mail client, process monitor, media player, file manager, chat |
-| package + `/etc` files + system service | 5 | login manager, display server, power and thermal daemons, package manager configuration |
-| package + files under the home | 6 | shell and prompt, the agent at the terminal, scripts, the sync client, a music player |
-| files under the home + user units + hooks | 2 | the desktop environment, audio |
-| third-party organisation tooling | 6 | out of scope here |
-
-**The desktop module alone** declares **88 files**, **4 flavors** (the window-manager stack, and
-one per class of machine), **2 user units** with a hook to enable them, 8 files under `/etc`, a
-wallpaper shipped as an asset, and reads **about 90 environment variables** as theme knobs,
-substituted into its templates at sync time and set through a theming tool. Its hook exists
-because *shipping a unit file does not run it*: one unit had been deployed for months and ran on
-one machine only, because somebody had enabled it there by hand.
-
-**The shell module** ships `~/.zshrc`, the prompt configuration, an `~/.ssh/config` that the
-predecessor generated from its registry, and a `LOGIN_SHELL` variable applied with `chsh` by a
-hook. Two flavors: the prompt theme, and autocompletion.
-
-**Other modules write into the desktop module's files.** The chat client places i3 and notifier
-snippets into `config.d` directories the desktop module owns, and its launch flags, window
-placement and notification colours are each a variable with a default.
-
-**One-off steps live in hooks** across the set: enable user units, `chsh`, create a swap file,
-`mkinitcpio`, enable a vendor VPN service the package ships disabled. Every one is state the
-host could declare or a verb a seat could serve; none is today.
-
-## 2. What the migration did with them
-
-The migration's module to-do scoped the whole set out as *desktop / workstation ricing — the
-workstation's own environment* and *node/OS tooling — managed on the node, never catalogue*. The
-last workstation's runbook then split the same set three ways: **A**, system scope, which the
-host's vocabulary can express today (the login manager, the display server, the power daemons,
-the package manager, the container runtime); **B**, under a home or a user unit, waiting on
-to-be 29; **C**, package only, the operator's call. The migration log closes the workstation with
-*the operator's desktop awaiting its design*.
-
-Two things followed from scoping them out. Nothing regenerates those files now, so a fix is a hand
-edit — the login manager's session script was fixed this way on the day of writing, and recorded
-in a repository nothing deploys from. And the one piece of this family written as a mesh module,
-the ssh client, was closed on hold in the catalogue until the controller carried the account fact.
-
-## 3. What the records already give
-
-| wanted | record | state |
-|---|---|---|
-| one module per managed thing; every module may have tools | [ADR 0040](../../02-DECISIONS/0040-what-a-module-is.md) | accepted; examples are services, and the shell is named as a *shared* seat |
-| a module declares its own node-scoped seat | [ADR 0121](../../02-DECISIONS/0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md), [ADR 0126](../../02-DECISIONS/0126-a-module-declares-its-own-seats.md) | accepted |
-| a seat's contract is its tools; a holder may add its own | [ADR 0132](../../02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md), [ADR 0170](../../02-DECISIONS/0170-the-firewall-seat-serves-its-verbs.md) | accepted; one node seat serves verbs live |
-| a capability the machine reports gates a holder | [ADR 0161](../../02-DECISIONS/0161-what-deserves-a-seat.md) §3 | accepted; the profile already reports `graphical-session` |
-| the account as a node fact; a file under the home owned by it | [to-be 29](../../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md) §1–2 | built in the controller; its record proposed in an open change |
-| inside a home: owned, written into, written by the module, found | proposed in the same change | proposed |
-| a setting declared with type, meaning, default and cost | proposed with the container-runtime records | proposed |
-| a managed file is derived; an edit is overwritten | [ADR 0011](../../02-DECISIONS/0011-managed-files-are-generated-never-edited.md) | accepted |
-| the mesh writes into a shared file, never over it | [ADR 0102](../../02-DECISIONS/0102-the-mesh-writes-into-a-shared-file-never-over-it.md) | accepted |
-| a module names no path; the host resolves the home | [ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md) | accepted |
-| the `user` shape: a login shell is declared state | [to-be 05](../../03-DESIGN/01-to-be/05-the-node-host.md) | designed; used by no module |
-
-## 4. What is missing
-
-1. **The account is recorded nowhere.** The node record has the column; on all four nodes it
- is empty. Every home-scoped module is unassignable until the operator states it.
-2. **User-scoped units.** The host's `service` shape has no user scope. To-be 29 says it
- plainly: *a workstation's per-user daemons have no form the mesh can send.* The desktop
- module's two units, the audio masks, the power module's memory guard and the thermal
- daemon's profile switcher all need it.
-3. **One-off steps.** `mkinitcpio`, `chsh`, creating a swap file. Each is either declared
- state the host lacks a shape for, or a verb a seat should serve. An action in a declaration
- is refused over the link, and rightly.
-4. **Settings leak** ([issue 168](../../04-ISSUES/168-a-setting-reaches-every-file-and-contribution/00-report.md)):
- a setting reaches every mergeable file and every contribution of its module. Ninety theme
- knobs on that mechanism would reach ninety files. The proposed settings record says a setting
- names the file it lands in; that has to ship first.
-5. **Where tools run.** Every module that serves a tool today does so from its own container
- per node. See [03](03-one-tool-executor-per-node.md).
-6. **A seat's verbs are undecided for every seat but three.** To-be 33 leaves which verbs each
- seat serves as *a decision per seat, slowly*. The environment adds a dozen seats.
-7. **Catalogue placement.** The media chain left this catalogue for its own; whether the
- environment does the same, and whether a third-party organisation's tooling belongs in a
- public catalogue, are unasked.
diff --git a/01-RESEARCH/018-the-operators-machine-as-modules/03-one-tool-executor-per-node.md b/01-RESEARCH/018-the-operators-machine-as-modules/03-one-tool-executor-per-node.md
deleted file mode 100644
index 65668853..00000000
--- a/01-RESEARCH/018-the-operators-machine-as-modules/03-one-tool-executor-per-node.md
+++ /dev/null
@@ -1,88 +0,0 @@
-# 03 — One tool executor per node
-
-*The direction the operator set on 2026-10-02, the evidence it rests on, and what it supersedes.
-A direction, not yet a decision: the record is written when this effort graduates.*
-
-## Where tools are served today
-
-[To-be 33](../../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md) names three families — a
-role's tools on the seat, a module's own tools on the module, the mesh's own verbs on the
-controller seat — and [ADR 0160](../../02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)
-says a runtime serves the subjects its membership issues. What *runs* that runtime is
-[ADR 0150](../../02-DECISIONS/0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md):
-one supervised process per module, under the module's own account, carrying that module's
-compiled tools. Measured on the live mesh:
-
-| who answers | how it runs | count |
-|---|---|---|
-| the mesh's own verbs | the controller binary, on its node | 17 verbs |
-| the store seat | the store's own runtime | 2 verbs |
-| the packet-filter seat | **a container per node**, built on the tool-runtime base image, with the network namespace and `NET_ADMIN`, on all four nodes | 3 verbs and 1 own tool |
-| every module's own tools | the module's container, one per node it runs on | 67 tools across the catalogue |
-| the console | a container per node, loopback MCP, `invokes: *` | serves none, calls all |
-| the host | — | serves nothing; answers no question about the machine |
-
-**The packet-filter holder is the case to look at.** The module is a package, three files and a
-system service. To serve three verbs it also declares a built image and a container on every
-node whose only job is to answer them. Scaled to the environment — a shell, a prompt, a launcher,
-a notifier, a compositor, a login manager, a service manager, a boot loader, a downloads folder —
-that is one container per module per node for software that is itself not a container, and the
-operator's judgement is that tools should not run inside a container at all.
-
-## The direction
-
-**One tool executor per node, on the host side.** A process the host supervises, the way the
-launcher supervises the host ([ADR 0005](../../02-DECISIONS/0005-the-node-host.md)): not a
-container, one bus credential for the node, module-agnostic. It loads the tool code of every
-module assigned to the node and serves each module's tools and each held seat's verbs on the
-subjects the membership issues — nothing changes in what [ADR 0159](../../02-DECISIONS/0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md)
-and [ADR 0160](../../02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)
-say about subjects, grants and memberships; what changes is that one process subscribes for the
-node instead of one per module.
-
-- **A module brings its tools as a built artifact**, a bundle the pipeline produces, never an
- image. The executor knows bundles and subjects; it knows nothing of zsh or nftables.
-- **A tool is code the module wrote**, one function behind an MCP verb. `execute` on the shell
- seat is a function with a string argument. The executor does not declare, template or
- interpret tools; it runs them.
-- **Root is the module's concern.** A tool that must change the packet filter or rebuild boot
- images escalates itself. The executor does not run as root for everyone, and the caller does
- not know.
-- **Any node may call any tool on any node.** The executor's credential may call everything,
- as the console's already does; per-module grants on the calling side are not kept.
-- **The mesh's own verbs stay with the controller** ([ADR 0154](../../02-DECISIONS/0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)),
- and a mesh-scoped seat's verbs run on the node that holds it
- ([ADR 0121](../../02-DECISIONS/0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md)).
- No hub is added; the controller's node is already one.
-
-**The console is the executor, renamed.** It already runs on every node with a credential that
-may call everything, and it already serves the mesh's tools to whoever is on the machine over
-MCP on loopback ([ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md)).
-It moves out of its container into the host's process tree, gains the serving half, and takes a
-name that says what it is — *the node's tool runtime* or simply *node tools*; "console" names
-the operator's half only.
-
-## What it supersedes, and what it keeps
-
-| record | effect |
-|---|---|
-| [ADR 0047](../../02-DECISIONS/0047-a-module-runs-its-code-as-its-own-process-with-its-own-account.md), [ADR 0150](../../02-DECISIONS/0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md) | superseded *for tools*: one process per node runs every module's tool code, under one account. A module's long-running service — a daemon, a container — is untouched; the executor runs tools, not services. The record must say why one account for every module's tools is acceptable: every tool may be called from every node anyway, and root is taken by the tool, not granted to the process |
-| [ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md) | kept in substance — a module, assigned per node, loopback MCP, the machine's login is the authority — changed in form: host-side, not a container; serves as well as calls; renamed |
-| [to-be 33](../../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md) §6, [to-be 34](../../03-DESIGN/01-to-be/34-the-console.md) | amended the same way |
-| [ADR 0170](../../02-DECISIONS/0170-the-firewall-seat-serves-its-verbs.md) §3, *a container may ask for a capability* | moot for that holder: the verbs run on the host side and escalate as they need |
-| the container-runtime seat, proposed in an open change: *the holder runs as a supervised process and serves the verbs locally to the host and on the bus* | consistent — a supervised process serving verbs is what the executor is; the open question is whether that holder keeps its own process or serves through the executor like everyone else |
-| the tool-runtime base image | no longer the way tools reach a node; may remain the way a module's *service* is built |
-
-## What stays open
-
-- **The executor's language.** The host is a static Go binary and loads no plugins, so the
- executor is a sibling process, and its language decides the language of every tool bundle.
- One decision, taken once.
-- **How a bundle reaches the node.** An artifact of the module's build, delivered as the host
- delivers everything else; whether it is a file resource in the declaration or a thing the
- executor fetches by digest.
-- **Reload.** A push that adds or upgrades a module's bundle reaches a running executor as a
- reload, not a restart, or every tool on the node blinks on every push.
-- **The host's own questions.** [Issue 160](../../04-ISSUES/160-a-machine-says-little-about-itself-and-only-when-asked/00-report.md)
- wants a machine to say more about itself. With an executor on every node, "what is this
- machine made of" is a seat verb like any other, served there.
diff --git a/01-RESEARCH/018-the-operators-machine-as-modules/04-the-seats-of-the-environment.md b/01-RESEARCH/018-the-operators-machine-as-modules/04-the-seats-of-the-environment.md
deleted file mode 100644
index fda9e879..00000000
--- a/01-RESEARCH/018-the-operators-machine-as-modules/04-the-seats-of-the-environment.md
+++ /dev/null
@@ -1,65 +0,0 @@
-# 04 — The seats of the environment
-
-*Candidates, not decisions. To-be 33 says which verbs a seat serves is a decision per seat,
-taken slowly, because a seat's tools bind every future holder. This document lists the roles the
-operator's machine has once, who could hold each, what gates it, and a first verb or two — so
-each record has a starting point.*
-
-## The rule for what is a seat here
-
-A role the machine fills **at most once** is a node-scoped seat, declared by the module family
-that fills it ([ADR 0126](../../02-DECISIONS/0126-a-module-declares-its-own-seats.md)). A thing
-several of which coexist without contention — editors, browsers, media players — is not a seat;
-each is a module with its own tools, and nothing is singular about it. A seat is held by one
-assignment per node; other modules of the same family may be installed beside it without
-holding it ([ADR 0040](../../02-DECISIONS/0040-what-a-module-is.md) §1, read with the sharper
-distinction: *installed* is not *holding*).
-
-## Candidate seats
-
-| seat | holders | gated by | first verbs |
-|---|---|---|---|
-| **login shell** | zsh, fish, bash | nothing: universal | `execute(command)`; `show-config`; the holding itself sets the account's login shell through the host's `user` shape |
-| **service manager** | systemd | the `service-manager` capability the profile reports | units: list, status, start, stop, restart, enable, journal; **user scope** on each |
-| **boot** | grub, systemd-boot | a machine that boots itself (not a container host) | `rebuild-images`; `entries` |
-| **package manager** | pacman, apt | the `package-manager` capability | search, installed, upgrade, orphans; today a capability the host uses, not a seat anyone holds |
-| **display server** | xorg, wayland compositors that are their own server | the `graphical-session` capability | `displays`; `layout` |
-| **display session** | i3, sway | the display server seat held on the node; i3 needs x11, sway needs wayland | `reload`; `workspaces`; `windows`; `move` |
-| **terminal emulator** | xterm, alacritty, foot | display session | `open`; `font` |
-| **launcher** | rofi, dmenu | display session | `show`; `theme` |
-| **notifier** | dunst, mako | display session | `send`; `history`; `rule` |
-| **compositor** | picom | display server (x11 only) | `restart`; `effects` |
-| **lock screen** | i3lock, swaylock | display session | `lock` |
-| **bar** | i3status-rust, waybar | display session | `reload`; `blocks` |
-| **login manager** | lemurs, greetd | graphical session | `sessions`; `default-session` |
-| **audio** | pipewire, pulseaudio | the machine reports a sound device | `sinks`, `sources`, `default`, `volume`, `mute` |
-| **clipboard** | greenclip, cliphist | display session | `history`; `clear` |
-
-Not seats, modules with their own tools: the editor, the browser, the mail client, the file
-manager, the media player, the chat client, the agent at the terminal, the downloads folder, the
-scripts folder, the sync client, the power and thermal daemons that are specific to one machine's
-hardware.
-
-## What the table implies
-
-**Capabilities come first.** `graphical-session`, `service-manager` and `package-manager` are
-reported today. *A display server is held* is not a capability but a seat being held, and a
-module that needs it declares a dependency on the seat, not on a capability: *i3 needs the
-display server seat held by xorg*. Whether a held seat can gate another's assignment is a
-question for the controller's resolver, and the first environment module after the shell will
-ask it.
-
-**The service manager comes early.** Four of the predecessor's modules ship user units, and the
-executor itself is a unit. User scope on the host's `service` shape is a host change whichever
-module holds the seat; the seat's holder answers the questions about units, it does not apply
-them — the host does, as it does for every declared resource.
-
-**The shell comes first.** Universal, no capability, one verb that is immediately useful on
-every node, and the `user` shape already makes the login shell declared state. It is the module
-that proves the pattern: a package, files under the home owned by the account, a seat claim,
-tools served by the executor, settings for the few things that vary per node, and a kept region
-for the operator's own lines.
-
-**The login manager is the first system-scope one**, because it needs nothing new: a package,
-two files under `/etc`, a service — the same shape the ssh daemon module has today — and the
-session script it owns is the file that was hand-fixed the day this effort opened.
diff --git a/01-RESEARCH/019-a-warm-twin-of-the-running-mesh/00-overview.md b/01-RESEARCH/019-a-warm-twin-of-the-running-mesh/00-overview.md
deleted file mode 100644
index 342636c0..00000000
--- a/01-RESEARCH/019-a-warm-twin-of-the-running-mesh/00-overview.md
+++ /dev/null
@@ -1,65 +0,0 @@
----
-status: abandoned
-initiated: 2026-10-02
-touches: [lab, the lab module, the catalogue, assignments, settings, the controller's store]
-became:
- - 02-DECISIONS/0237-a-change-is-judged-against-the-mesh-that-runs-before-it-merges-on-the-build-seat.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
- - 03-DESIGN/01-to-be/45-a-core-that-cannot-fail-silently.md
----
-
-# 019 — A warm twin of the running mesh
-
-## What is being investigated
-
-Whether the lab can keep a **warm twin of the mesh as it actually runs**: the same machines, carrying
-the same catalogue, the same assignments and the same settings as the live mesh, raised once and kept
-ready, so that a change can be tested against the mesh as it is rather than against a scenario
-written to resemble it. A run against the twin would go through the lab module like any other run:
-a branch per repository, the twin restored from its snapshot, the change applied, the beds run.
-
-## Why
-
-The lab's beds raise meshes from declarations written for the bed. They prove the mechanism. They
-do not prove that a change works on the mesh that runs, with its accumulated assignments, its
-operator settings, its adopted machines and its modules in their real combinations. The gap showed
-on 2026-10-02:
-
-- a change to how a module's settings reach its files was correct in every bed, and would have put a
- setting into the container runtime's configuration on every machine running that module. Only the
- composed plan for a real machine showed it;
-- a firewall change composed cleanly and still left one machine's wired port unfiltered, because
- of a link that machine had and no bed did;
-- a recovery step was needed on every machine at once, after a change that every bed had passed.
-
-The lab already has a warm mode, a snapshot of a raised scenario restored between attempts. What it
-does not have is a scenario that **is** the running mesh, kept current with it.
-
-## What it touches
-
-- **What a twin is made of.** The catalogue and the assignments are records; settings are records;
- secrets are sealed to machines and cannot be copied. Which of these can be carried to the lab as
- they are, which must be substituted, and how a twin says what it substituted.
-- **Data.** A twin with the real catalogue and no real data proves composition and delivery, not a
- migration. Whether a twin carries data, a sample of it, or none.
-- **Keeping it current.** A twin raised once goes stale with the first merge. Whether it is
- re-derived from the live records on each run, refreshed on a schedule, or rebuilt only when asked.
-- **Machines.** The live mesh has machines of different kinds: a server on the internet, machines
- behind a home router, a laptop that sleeps. Which of their properties a twin must reproduce for a
- test to mean anything (reachability, the private network, the found firewall).
-- **Cost.** The lab machine's memory and disk, and how long a twin takes to raise from cold.
-- **The lab module's tools.** A run against the twin rather than a named bed: one more tool, or an
- argument to the run tool.
-
-## Starting point
-
-The lab module (ADR 0172) runs beds through the mesh, and the lab's warm mode already snapshots and
-restores a raised scenario. The beds that raise a machine shaped like one live machine from the
-catalogue are the nearest existing thing, and the first to compare against.
-
-## What overtook it
-
-*Closed 2026-10-10.* No twin was built. The live mesh was already the test bed ([ADR 0149](../../02-DECISIONS/0149-the-live-mesh-is-the-test-bed.md)). The half of the problem that a twin was
-for, a change composing wrongly for a real machine, is the merge gate: a change is judged against the
-mesh that runs before it merges ([ADR 0237](../../02-DECISIONS/0237-a-change-is-judged-against-the-mesh-that-runs-before-it-merges-on-the-build-seat.md), [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)), from a snapshot of the mesh's facts. Replaying
-incidents is carried by the core's scripted replays in [to-be 45](../../03-DESIGN/01-to-be/45-a-core-that-cannot-fail-silently.md).
diff --git a/01-RESEARCH/020-what-a-bundled-tool-is-given/00-overview.md b/01-RESEARCH/020-what-a-bundled-tool-is-given/00-overview.md
deleted file mode 100644
index 38a91ebc..00000000
--- a/01-RESEARCH/020-what-a-bundled-tool-is-given/00-overview.md
+++ /dev/null
@@ -1,34 +0,0 @@
----
-status: graduated
-initiated: 2026-10-03
-touches: [the tool runtime, the catalogue's tool bundles, the controller's declaration composer, settings, own secrets, 03-DESIGN/01-to-be/38-building-the-operators-machine.md]
-became: [02-DECISIONS/0192-a-tools-bundle-declares-what-it-is-given-and-the-runtime-hands-it-to-that-bundle-alone.md, 03-DESIGN/01-to-be/38-building-the-operators-machine.md]
----
-
-# 020 — What a bundled tool is given
-
-## What is being investigated
-
-How a module's tools, once they are a bundle the node's runtime loads
-([ADR 0175](../../02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md),
-[ADR 0188](../../02-DECISIONS/0188-a-modules-own-code-is-bundles-in-any-language-and-a-tools-bundle-speaks-mcp-to-the-runtime.md)),
-learn the things their container used to be handed: where the module's configuration file is, where
-its token or password is, which port the service listens on, where a provision's address is written.
-A container is given these as an environment and mounts, composed by the mesh per module per machine.
-A bundle has no environment of its own: the runtime's process carries four words for every bundle it
-loads, and nothing per module ([design 38](../../03-DESIGN/01-to-be/38-building-the-operators-machine.md) WP4).
-
-## Why
-
-Two holders moved on 2026-10-03 — the packet filter and the intrusion prevention — and both could,
-because neither needs anything but a fixed path and root. Of the thirty-three modules whose tools
-still run as containers on the runtime's image, thirty-one are not like that: their environment
-names a configuration file, a credential file, a service address, a grants directory. Moving them
-one by one without a rule for this would give the mesh thirty-one answers to one question. The
-measurement and the options are in [01](01-what-the-containers-are-given.md).
-
-## What it touches
-
-The runtime (which hands a bundle what it is given), the composer (which resolves `${dir:…}` and
-`${port:…}` for a container today and would for a bundle), the manifest (where a bundle would say
-what it needs), and design 38, which records the gap and must say the rule once there is one.
diff --git a/01-RESEARCH/020-what-a-bundled-tool-is-given/01-what-the-containers-are-given.md b/01-RESEARCH/020-what-a-bundled-tool-is-given/01-what-the-containers-are-given.md
deleted file mode 100644
index 65e62e04..00000000
--- a/01-RESEARCH/020-what-a-bundled-tool-is-given/01-what-the-containers-are-given.md
+++ /dev/null
@@ -1,86 +0,0 @@
-# What the tool containers are given, measured
-
-Counted 2026-10-03 in the catalogue, after the two holders moved.
-
-| | |
-|---|---|
-| modules whose tools still run as a container on the runtime's image | 33 |
-| tool containers among them (two modules run two) | 36 |
-| modules whose container's environment carries only the bus credential | 1 (the intrusion prevention, now moved) |
-| modules whose container's environment carries more | 32 — 31 still containers |
-
-## What "more" is
-
-Every value a container is given is one of five shapes. The reference kinds the composer resolves
-in those values, over the 36 containers: a module directory (`${dir:…}`) in all 36, a mesh-chosen
-port (`${port:…}`) in 12, a seat and an access grant once each.
-
-1. **A file the mesh already places on the host, mounted in.** The module's configuration as
- JSON (`…_CONFIG_FILE`), its own secret (`…_TOKEN_FILE`, `…_PASSWORD_FILE`, `MESH_BROKER_FILE`),
- a provision's address and secret written for it. Every one is a path under one of the module's
- directories — its mesh state, its state, its grants, what it has written — mounted at a path of
- the container's choosing and named to the tool through the environment. **The file is on the
- host already; only the name under which the tool finds it is the container's.**
-2. **The service's address, with the port the mesh chose:** `http://127.0.0.1:${port:3000}`. The
- port is the composer's; the rest is the manifest's constant.
-3. **A provision's address as a constant string** (a database's URL on the module's own network
- name), paired with a mounted secret file from shape 1.
-4. **A directory of grants** (`MESH_RECEIVES`): shape 1 again, a directory rather than a file.
-5. **Literals the image needs:** a time zone, a user id, a memory limit. These belong to the
- service's container where one exists; a tool bundle needs none of them.
-
-So the whole of what a bundled tool needs is: the paths of its module's directories on this
-machine, the ports the mesh chose for its module here, and the constants its own manifest wrote.
-Nothing a container had that a bundle cannot have; the mesh composes all three for the container
-today, per module per machine.
-
-## What the runtime already has for it
-
-- The SDK's tool contributor is `(env) => tools`, and `collectTools(env)` takes the environment to
- hand each contributor. The runtime calls it without one, so every contributor reads the process's
- — the four words. The hook for a per-module environment exists and is unused.
-- A launched bundle ([ADR 0188](../../02-DECISIONS/0188-a-modules-own-code-is-bundles-in-any-language-and-a-tools-bundle-speaks-mcp-to-the-runtime.md))
- is spawned with the runtime's environment; the launch takes an environment argument.
-- The composer resolves `${dir:…}` and `${port:…}` for a container's `env` and `volumes`; the same
- resolution over a bundle's declaration is the same code.
-
-## Options
-
-**A. The bundle declares its environment on its artifact, and the mesh composes it as a
-container's.** The manifest's tools artifact gains `env`, resolved with the same references;
-values that were mount targets become the host-side paths directly (`${dir:mesh-state}/config.json`
-rather than `/run/config/config.json`). The controller composes one environment per bundle per
-machine into the runtime's declaration; the runtime hands it to the bundle's contributor and to a
-launched child, and to nothing else. *For:* the tool code does not change — it reads the same
-names; the conversion of the thirty-one is a mechanical move of the container's `env` with the
-mounts folded in; one rule, one place. *Against:* the runtime's process carries thirty-one
-environments in its declaration, and a bundle's environment is visible to the other bundles in the
-process unless the runtime keeps them apart, which it must — a tool that reads `process.env`
-instead of the environment it was handed would see its neighbours' paths.
-
-**B. The runtime derives the environment from the module's placed manifest.** No new field: the
-runtime reads, for each module it serves, where that module's directories and ports are, and hands
-a conventional set of words. *For:* nothing to declare. *Against:* a convention the tool code must
-be rewritten to, thirty-one times; the runtime learns the composer's job; a module that names its
-file `config.json` and one that names it `settings.json` need different words anyway.
-
-**C. Tools read their module's files through the bus** — ask the controller. *Against:* a tool
-that cannot start without the bus answering a question is a tool that fails in the one case the
-tools exist for, and a secret crossing the bus to reach a file already on the machine is a
-disclosure for nothing.
-
-A is the one that keeps the tool code and the composer's vocabulary as they are, and names the one
-thing the runtime must add: an environment per bundle, kept apart. The thing to decide beside it:
-whether a bundle's environment may name a secret file at all, or whether secrets stay mounts in
-spirit — a path the tool reads, never a value in the environment — which is what every container
-does today and what A keeps if the rule says *paths, not values*.
-
-## What a decision would have to say
-
-- Where a bundle says what it is given (the artifact, option A), and that values are paths and
- constants, never a secret's content.
-- That the composer resolves it with the references it already has, per module per machine.
-- That the runtime hands each bundle its own environment and nothing of another's, and how that is
- checked: a test loading two bundles whose environments differ and asserting each sees only its own.
-- That the thirty-one move in one mechanical change after the rule lands, each proven by its tools
- answering from the runtime, and the registration gate then refuses the container shape for all.
diff --git a/01-RESEARCH/021-finding-a-tool-in-the-mesh/00-overview.md b/01-RESEARCH/021-finding-a-tool-in-the-mesh/00-overview.md
deleted file mode 100644
index 3f8d7747..00000000
--- a/01-RESEARCH/021-finding-a-tool-in-the-mesh/00-overview.md
+++ /dev/null
@@ -1,48 +0,0 @@
----
-status: graduated
-initiated: 2026-10-03
-touches: [the console, 03-DESIGN/01-to-be/34-the-console.md, the tool runtime, seats, assignments]
-became: [02-DECISIONS/0195-the-meshs-tools-are-found-by-address-not-announced-whole.md, 03-DESIGN/01-to-be/34-the-console.md]
----
-
-# 021 — Finding a tool in the mesh
-
-## What was investigated
-
-How an agent finds the one tool it needs among everything the mesh answers, and how a call names
-exactly what it asks — a role the mesh holds once, a role every machine holds, or one assignment of a
-module on one machine — rather than receiving the whole catalogue and a name that can mean several
-things.
-
-## Why
-
-The operator's observation on 2026-10-03: *Claude should not see all tools at once; they should be
-discoverable — and `postgres.list_databases` is wrong, asking one machine's postgres is not asking
-another's.* Measured the same day from the console's own answer:
-
-| | |
-|---|---|
-| tools announced to every session at its start | 228, in 110 KB |
-| names (module or seat prefixes) | 43 |
-| node seats' verbs, which require `node` | 22 |
-| modules with tools on more than one machine | 4 — fail2ban, nftables (every machine), postgres, mssql (two each) |
-| modules reported "not answering", most with no tools and several retired | 47 |
-
-The two stateful modules on two machines are listed **once**, with `node` optional and *whichever
-answers* when it is left out — though their two instances hold different databases. Design 34 §3 says
-such a module is listed once per machine; the live console does not do that. The list is taken once
-per session, so a tool that arrives later is invisible until the client reconnects. And only Claude
-Code's own deferral of long tool lists keeps the 228 from the model's context; another MCP client
-would receive them whole.
-
-## Options
-
-1. **Keep the flat list; rely on the client to defer it.** Rejected: a property of one client, and it
- leaves the ambiguity and the stale list.
-2. **One flat tool per assignment** (`ace_postgres_list_databases`). Removes the ambiguity, multiplies
- the list, and runs into the API's tool-name limit (letters, digits, `_`, `-`, 64 characters).
-3. **A small fixed set of tools that walk the mesh's own structure**, with the full address as an
- argument: the mesh's seats; a machine's node seats and assignments; a search; a description; a
- call. Chosen — see [ADR 0195](../../02-DECISIONS/0195-the-meshs-tools-are-found-by-address-not-announced-whole.md).
-4. **MCP resources or prompts for discovery.** Clients support them unevenly, and an agent acts
- through tools; a resource it cannot be relied on to read is not a discovery path.
diff --git a/01-RESEARCH/022-where-a-modules-long-running-code-runs/00-overview.md b/01-RESEARCH/022-where-a-modules-long-running-code-runs/00-overview.md
deleted file mode 100644
index 86b4f32a..00000000
--- a/01-RESEARCH/022-where-a-modules-long-running-code-runs/00-overview.md
+++ /dev/null
@@ -1,41 +0,0 @@
----
-status: graduated
-initiated: 2026-10-03
-touches: [the tool runtime, the per-module containers, the SDK, the bus grants, 03-DESIGN/01-to-be/38-building-the-operators-machine.md]
-became: [02-DECISIONS/0198-a-modules-long-running-code-is-launched-by-the-node-runtime-and-reaches-the-bus-through-it.md, 03-DESIGN/01-to-be/38-building-the-operators-machine.md]
----
-
-# 022 — Where a module's long-running code runs
-
-## What was investigated
-
-Twenty-three modules still run their own code in a container built on the runtime's image. Their tools
-can move as bundles ([ADR 0192](../../02-DECISIONS/0192-a-tools-bundle-declares-what-it-is-given-and-the-runtime-hands-it-to-that-bundle-alone.md),
-[ADR 0193](../../02-DECISIONS/0193-every-bundle-the-runtime-serves-is-launched-and-the-runtime-knows-no-language.md));
-the rest of what those containers run cannot yet. This asks where that code goes and how it reaches
-what its container handed it.
-
-## What that code is, measured 2026-10-03
-
-| | modules |
-|---|---|
-| subscribes to events on the bus | audit-logger (everything), mesh-catalog (two seat events), mesh-vault, records (`gitea.pull.merged`), and postgres, mongodb, mssql, redis, mosquitto logging their own lifecycle |
-| provisioners: read the grants the mesh delivered as files, act on the backend, emit | 12 |
-| a run-once preparation step | mesh-catalog |
-| a command-line client of the backend | psql, mosquitto_ctrl, git (packages on every machine's system); mongosh, sqlcmd (not in its repositories) |
-| a service reached by a container name | icecast, mailu-admin, minio, mongodb-server, mssql |
-| a main of its own | anthropic-consumer, openai-consumer, route-adapter |
-
-A provisioner needs nothing a launched bundle lacks: files named by its words, its backend, and an emit
-that already travels through the runtime. The one thing missing is **a subscription** — events
-delivered to the module's code, acknowledged when it has handled them.
-
-## Options
-
-1. **The runtime launches it and is its bus**: the stdio channel gains a subscription; the runtime
- binds the module's durable consumer and delivers each event to the child, acknowledging when the
- child answers. One bus connection per machine; any language. Chosen.
-2. **A process per module with its own bus client and credential.** Every language's SDK would carry
- a transport and every module a credential on disk — what ADR 0188 rejected for tools, for the same
- reasons.
-3. **Keep the containers for this code.** Leaves ADR 0188's rule broken for 23 modules indefinitely.
diff --git a/01-RESEARCH/023-a-seat-protocol-that-defines-what-its-holder-owns/00-overview.md b/01-RESEARCH/023-a-seat-protocol-that-defines-what-its-holder-owns/00-overview.md
deleted file mode 100644
index 2b96d156..00000000
--- a/01-RESEARCH/023-a-seat-protocol-that-defines-what-its-holder-owns/00-overview.md
+++ /dev/null
@@ -1,38 +0,0 @@
----
-status: graduated
-initiated: 2026-10-03
-touches: [the seats, the seat protocol, the controller's ownership check, 03-DESIGN/01-to-be/26-the-seats.md]
-became: [03-DESIGN/01-to-be/26-the-seats.md, 02-DECISIONS/0287-a-seat-names-the-paths-its-holder-owns-and-a-claim-without-them-is-refused.md]
----
-
-# 023 — A seat protocol that defines what its holder owns
-
-## What is investigated
-
-**A seat is a definition — a protocol — and a module occupies it by implementing that protocol.**
-Today the protocol is what the holder accepts, emits and serves (ADR 0118, 0129, 0132): its verbs, as MCP
-tool definitions. This asks whether the protocol should also name the **files and directories the
-holder owns**, so that occupying the seat means owning them: `node-resolver-config` owns
-`/etc/resolv.conf`, `node-hosts-file` owns `/etc/hosts`, the intrusion prevention owns its jail file.
-
-The direction is the protocol's, not the holder's: the seat states what any holder must own; a module
-that wants the seat must declare those paths among its resources, or the controller refuses the claim
-as not implementing the seat. Two seats may not name one path.
-
-## Why
-
-Who owns a singular file is today answered by reading every manifest, and enforced only after the fact,
-when two modules on one machine both declare the same path. The question *which module owns
-`/etc/resolv.conf`?* came up on 2026-10-03 with no place to look it up. A seat that names the path answers
-it from the seat table, before any module is written, and makes "implements the seat" checkable.
-
-## What it touches
-
-- The seat definition and its table (ADR 0122) — a new part of the protocol.
-- The controller's ownership check (`checkResources`), which already refuses two modules owning one path.
-- Every node seat that is really about a file: `node-resolver-config`, `node-hosts-file`
- ([ADR 0199](../../02-DECISIONS/0199-a-module-that-answers-names-declares-its-zone-and-a-nodes-hosts-file-is-one-modules.md)),
- `node-intrusion-prevention`, `node-packet-filter`.
-
-Raised by the operator during the resolver work of ADRs 0194–0199 and parked there so that work was not
-widened by it.
diff --git a/01-RESEARCH/024-state-a-module-keeps-on-the-bus/00-overview.md b/01-RESEARCH/024-state-a-module-keeps-on-the-bus/00-overview.md
deleted file mode 100644
index 91bc89ea..00000000
--- a/01-RESEARCH/024-state-a-module-keeps-on-the-bus/00-overview.md
+++ /dev/null
@@ -1,152 +0,0 @@
----
-status: graduated
-initiated: 2026-10-04
-touches: [the bus, what a module declares, the tool runtime, the SDK, the bus grants, 03-DESIGN/01-to-be/25-the-bus-on-nats.md, 03-DESIGN/01-to-be/32-what-a-module-declares.md]
-became: [02-DECISIONS/0201-a-module-keeps-its-current-state-in-key-value-buckets-it-declares-and-reaches-through-the-runtime.md, 03-DESIGN/01-to-be/32-what-a-module-declares.md, 03-DESIGN/01-to-be/25-the-bus-on-nats.md]
----
-
-# 024 — State a module keeps on the bus
-
-## What is investigated
-
-A place on the bus where a module's own code keeps **current state** — not history — that every
-machine sees, including a machine that joins after the state was written: put, get, delete, list and
-watch, reached through the node's runtime the way a bundle already publishes, asks and subscribes
-([ADR 0198](../../02-DECISIONS/0198-a-modules-long-running-code-is-launched-by-the-node-runtime-and-reaches-the-bus-through-it.md)).
-On NATS that is a key-value bucket. The questions are what a module declares, who creates the
-bucket, what the grants are, what the runtime's verbs are, and what may never be stored.
-
-## Why
-
-The mesh carries two kinds of module traffic and a third is missing.
-
-- **Events** land in the EVENTS stream: limits retention, seven days, ten thousand messages per
- subject, a durable consumer per consuming module that replays what it missed. Never a secret
- ([design 32](../../03-DESIGN/01-to-be/32-what-a-module-declares.md) §10).
-- **Requests** are core request/reply — tool calls, a bundle's `mesh/ask` — and are kept nowhere.
-
-Neither is *the current value of something*. Two cases from the first module that needs it, the
-operator's agent on a machine ([design 36](../../03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md)):
-
-1. **An MCP server registered for every machine.** Registering emits an event every machine's copy
- of the module consumes. A machine the module is assigned to *after* the registration has no
- durable consumer yet — the consumer is created at assignment — so it never hears of it. Wanted
- instead: one entry per server, for every machine or for one; every machine reads the whole current
- set when it starts and watches for changes; unregistering is a delete; any machine can list it.
-2. **Which licence a machine is bound to** ([design 39](../../03-DESIGN/01-to-be/39-the-anthropic-licence-manager.md)).
- As events, a machine that was off for a day replays every rotation since and asks for a token
- after each. It needs only the latest binding and its generation. The token itself stays on
- request/reply and is never stored.
-
-The design already expects this. [Design 25](../../03-DESIGN/01-to-be/25-the-bus-on-nats.md) §1:
-"conditions and observed state in key-value buckets that anything may watch".
-[Research 017](../017-a-mesh-that-heals-itself/01-the-intended-behaviour.md) wants a provisioner's
-"what I applied" and a rotation's step kept in one rather than in memory. Nothing implements it.
-
-## What exists, measured 2026-10-04
-
-| | fact | where |
-|---|---|---|
-| streams | five kinds of mesh stream: CONTROL (work queue), NODES and ASSIGNMENTS (last per subject), EVENTS (limits: 7 days, 10 000 per subject), one work queue per seat that accepts | the controller's broker streams |
-| the state relationship | [design 32](../../03-DESIGN/01-to-be/32-what-a-module-declares.md) §4 already names *state* — 1:1, last per subject — and says it is "declared: the mesh's own". Two streams use it, both written by the controller. No module can declare it | design 32, the controller |
-| key-value buckets | none, anywhere | all four code repositories |
-| the runtime's bus verbs | `mesh/publish`, `mesh/ask`, `mesh/subscribe`; delivery back to the bundle is `mesh/event` | the runtime's launcher |
-| the runtime's principal | one bus user per machine carries every assigned module; its grant is the union of theirs. That one module's code does not act as another is the runtime's to keep: it publishes under the module's own name by construction | the controller's grant composition, the runtime's bus |
-| what a bundle is issued | a membership per assignment, last per subject, read directly by the runtime: where it serves, where it emits, what it reaches | ADR 0160 |
-| who creates bus objects | the controller only — mesh streams on every raise, a seat's stream at registration, a module's consumer at assignment. No module reaches the JetStream API | design 25 §3 |
-
-### What a key-value bucket needs from a grant, against a real server
-
-Measured against nats-server 2.10 with the Go client the runtime already uses, a bucket created by
-an unrestricted user and used by two users holding only the subjects below (`B` is the bucket):
-
-| operation | subject published | writer | reader |
-|---|---|---|---|
-| bind to the bucket | `$JS.API.STREAM.INFO.KV_B` | yes | yes |
-| get | `$JS.API.DIRECT.GET.KV_B.>` | yes | yes |
-| put, delete | `$KV.B.>` | yes | **refused** |
-| list keys, watch | `$JS.API.CONSUMER.CREATE.KV_B.>` — an ordered, ephemeral consumer | yes | yes |
-| stop a watch cleanly | `$JS.API.CONSUMER.DELETE.KV_B.>` | yes | yes |
-| answers | its own inbox, which every principal already subscribes | — | — |
-
-*Checked again once built, 2026-10-04:* the grants the controller composes for two machines' runtimes —
-one carrying the owner, one only a reader — were loaded into a server as composed, and each operation
-was run as each runtime's user. The owner's did all of them; the reader's read, listed and watched,
-and its put and delete were refused by the server.
-
-Three things the measurement showed that reading the documentation would not have:
-
-1. **A refused put is not an error to the caller; it is a timeout.** The server reports the
- permission violation asynchronously, on the connection, and the client waits out its deadline
- for an acknowledgement that never comes. So a runtime that relies on the grant alone tells a
- bundle "timed out" for "you may not write this" — it must refuse first, from what the module was
- issued, with the reason.
-2. **A watch's current values include deletions.** A key deleted earlier arrives among the initial
- values as a delete marker, before the end-of-current marker. A bundle asking "what is there now"
- must not be handed those.
-3. **Without the consumer-delete grant, stopping a watch hangs** until its deadline, and the
- ephemeral consumer lingers on the server until it times out by itself.
-
-### Whether the events shape is enough instead
-
-Honestly compared, because a new primitive is a cost:
-
-- **EVENTS cannot be made last-per-subject for some subjects.** Retention is per stream, and
- JetStream refuses a second stream overlapping the first (verified and recorded in design 32 §3).
- A state subject inside `mesh.mod.*.event.>` keeps EVENTS' seven days: a licence binding unchanged
- for a week disappears.
-- **A separate last-per-subject stream per module** is possible — it is exactly what a key-value
- bucket *is* on the server: a stream with one message per subject, a rollup for purge, and direct
- reads. Building it by hand gives up the client's get, list, delete and watch, which are the
- operations both cases need, and would be the mesh writing NATS's own key-value layer again.
-- **Consumers are the wrong reader.** A durable consumer per reading module is created at
- assignment and replays from where it is; state wants "everything current, now, then changes",
- which an ordered ephemeral consumer from the last value per subject gives and a durable does not.
-
-So key-value is not a convenience over events; it is the state relationship design 32 already
-names, opened to modules.
-
-## Questions, and what this effort proposes
-
-1. **What a manifest says.** `state` names the buckets a module owns, by local name — every
- instance of the module may write them and read them. `reads` names another module's bucket as
- `.`, read-only. Names only, never a bucket or subject (design 32 §1). A bucket's
- options — how many past values it keeps, how long a value lives — are the owner's to declare,
- the way a seat declares its own retention (design 32 §3).
-2. **Scope.** One bucket per module per name, mesh-wide. A key may carry a machine by the module's
- own convention (`all.`, `.`). A bucket per machine was considered and
- not proposed: "list every server for every machine" becomes a walk over buckets, and the grant
- could only narrow writes, which nothing asked for — every instance of the owner already writes.
-3. **Who creates the bucket.** The controller, from the catalogue, on every raise — a bucket exists
- from registration, like a seat's stream, so a reader can watch before the owner is assigned
- anywhere. Never a module.
-4. **The runtime's verbs.** `mesh/state.get`, `mesh/state.put`, `mesh/state.delete`,
- `mesh/state.keys`, `mesh/state.watch`, each naming the bucket as the module named it. A watch
- is answered once the current values are on their way, then each change is delivered to the
- bundle as a `mesh/state` request it answers — current values first (no deletions among them), an
- end-of-current marker, then changes. A child that restarts watches again, as it subscribes
- again. The runtime refuses, with the reason, a bucket the module was not issued, and a write to
- one it only reads.
-5. **Secrets.** None in a bucket, sealed or not: a bucket is a stream (design 32 §10). Sealed values
- are plain base64 and cannot be recognised, so the mechanical check is partial and said to be: the
- runtime refuses a value carrying a field whose name says it is a credential (`password`,
- `secret`, `token`, `authorization`, …), which catches the ordinary mistake and not a determined
- one. For the first consumer this has a concrete consequence: an MCP server registered with an
- authorisation header keeps that header out of the bucket.
-6. **History, lifetime, size.** One value per key unless the owner says more; no expiry unless it
- says one; a value at most 256 KiB and a bucket at most 64 MiB, the mesh's caps rather than a
- module's. **A bucket outlives its module's assignment** — what a module stored is data, and data
- outlives what declared it ([ADR 0030](../../02-DECISIONS/0030-data-outlives-the-mesh-that-declared-it.md));
- unassigning is not cleaning up. A bucket whose declaration is gone is reported, never removed.
-7. **Events or state.** State (above).
-
-## The work, once decided
-
-1. A decision record, then design 32 (*state* becomes a relationship a module declares) and design
- 25 (key-value buckets are part of the bus) amended.
-2. The controller: the manifest's two words and their registration check; buckets asserted on every
- raise; the grants for owners' and readers' runtimes; the buckets issued in each membership.
-3. The runtime: the five verbs, the watch delivery, the refusals; tested against a real server.
-4. The SDK, TypeScript and Go: a small state surface over the verbs.
-5. Proved on a running mesh with one small module, then handed to the operator's agent, whose
- registered servers move from events to a bucket.
diff --git a/01-RESEARCH/025-how-a-module-plugs-into-the-shell/00-overview.md b/01-RESEARCH/025-how-a-module-plugs-into-the-shell/00-overview.md
deleted file mode 100644
index a0eee3c1..00000000
--- a/01-RESEARCH/025-how-a-module-plugs-into-the-shell/00-overview.md
+++ /dev/null
@@ -1,94 +0,0 @@
----
-status: graduated
-initiated: 2026-10-04
-touches:
- - 02-DECISIONS/0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md
- - 02-DECISIONS/0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md
- - 02-DECISIONS/0177-a-unit-may-be-user-scoped-and-the-service-manager-is-a-node-seat.md
- - 02-DECISIONS/0126-a-module-declares-its-own-seats.md
- - 02-DECISIONS/0102-the-mesh-writes-into-a-shared-file-never-over-it.md
- - 02-DECISIONS/0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md
- - 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
- - 03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md
- - 03-DESIGN/01-to-be/31-a-module-declares-its-fail2ban-jail.md
- - 03-DESIGN/01-to-be/37-the-operators-machine.md
- - 03-DESIGN/01-to-be/38-building-the-operators-machine.md
- - 04-ISSUES/168-a-setting-reaches-every-file-and-contribution/00-report.md
-became:
- - 02-DECISIONS/0203-the-accounts-environment-is-one-modules-and-every-module-contributes-to-it.md
- - 02-DECISIONS/0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md
- - 02-DECISIONS/0205-software-the-distribution-does-not-package-ships-as-a-pinned-archive-of-the-module.md
- - 03-DESIGN/01-to-be/41-the-shell-and-the-accounts-environment.md
----
-
-# 025 — How a module plugs into the operator's shell
-
-## What is investigated
-
-The shell module writes the mesh's part of the account's shell startup file. But the shell is not the
-only module that needs a line there. A prompt theme loads itself from it. A language version manager
-sets a variable and sources its loader. A toolchain puts its directory on `PATH`. A desktop module
-names the browser. Today all of these sit in one hand-written file, and the shell module as written
-carries some of them in its own block and loads others only "if a module placed them". Nothing says
-how they get placed.
-
-This effort asks four things:
-
-1. **How a module contributes to the shell**: what it declares, who composes it, and in what order it
- lands.
-2. **Where the environment lives.** Variables and `PATH` entries are facts about the account, not
- lines of one shell's syntax. They should reach every shell (interactive or not), the login shell's
- `execute` verb, and programs a graphical session starts.
-3. **Where the operator's own lines go,** so that assigning the shell module loses nothing the
- machine does today.
-4. **Which part of a file the mesh owns.** ADR 0174 calls the kept region the operator's; the host and
- to-be 38 implement the inverse (the mesh owns a marked block, and everything outside it is the
- operator's). The record this becomes says which.
-
-## Why
-
-Rolling out the shell module (to-be 38 WP5) was stopped on 2026-10-04 after a review of what assigning
-it would do. Measured in [01](01-what-the-shell-file-holds-today.md):
-
-- Every machine carries the same predecessor-written startup file, so the module's block would be
- appended after its own older copy and everything would run twice.
-- The block drops lines the machines rely on today.
-- Nothing installs the prompt theme or the plugins the block loads.
-- The `execute` verb runs a non-interactive login shell, which never reads the file the block is
- written into.
-
-The operator's direction: other modules must be able to plug themselves into the shell; the prompt
-becomes its own module; assigning the shell module must lose no functionality; and the environment,
-`PATH` above all, needs an answer of its own.
-
-## What it touches
-
-- The manifest. A contribution to the shell is either a new use of the existing `contributes` /
- `receives` pair or a new gathered field like `jails` (to-be 31).
-- The controller's composition, if the controller assembles the text.
-- The `login-shell` seat (ADR 0176): what a holder must do with what is contributed to it, and
- whether a module or the mesh declares the seat. Possibly a new seat for the environment, beside it
- and beside the service manager's (ADR 0177). Research 023 asks the related question of a seat
- naming the files its holder owns.
-- ADR 0174's wording of the kept region, and ADR 0182's classification of the paths under a home.
-- The zsh module, and the modules this makes possible: an environment module, the prompt, a version
- manager, a toolchain.
-
-## Where it stands
-
-The operator proposed a separate **environment module**: one module, holding a mesh seat of its own,
-that alone writes the account's environment. It writes a file that shells source and the service
-manager's user environment, from the variables and `PATH` entries every other module contributes to
-it. That is the starting position for the environment ([02](02-how-a-module-plugs-in.md) §1, option
-E6). It leaves the shell's contribution as shell code only (§2), addressed to the `login-shell` seat,
-which moves into the mesh's own seat set beside the new `node-environment` (§6).
-
-Graduated on 2026-10-04 with one change from the starting positions: the controller, not the
-environment module's own code, renders the environment into the module's files, so that the result
-is in the declaration before a machine applies it ([ADR 0203](../../02-DECISIONS/0203-the-accounts-environment-is-one-modules-and-every-module-contributes-to-it.md),
-option 6b).
-
-## Documents
-
-- [01 — What the shell file holds today](01-what-the-shell-file-holds-today.md): evidence.
-- [02 — How a module plugs in](02-how-a-module-plugs-in.md): the options and the starting position.
diff --git a/01-RESEARCH/025-how-a-module-plugs-into-the-shell/01-what-the-shell-file-holds-today.md b/01-RESEARCH/025-how-a-module-plugs-into-the-shell/01-what-the-shell-file-holds-today.md
deleted file mode 100644
index 49f468b2..00000000
--- a/01-RESEARCH/025-how-a-module-plugs-into-the-shell/01-what-the-shell-file-holds-today.md
+++ /dev/null
@@ -1,103 +0,0 @@
-# 01 — What the shell file holds today
-
-Measured 2026-10-04 on the four machines of one installation: two servers and two workstations. All
-four have the account's login shell set to zsh, zsh installed from the distribution, and a
-predecessor-written startup file. The predecessor is retired, so nothing manages these files any more.
-
-## The startup file is the same everywhere
-
-The account's `~/.zshrc` is **byte-identical on all four machines**: 102 lines, one checksum.
-`~/.zshrc.local`, which the last line of `~/.zshrc` sources, comes in **two variants**: one shared by
-both servers, and one shared by both workstations. So the "per-machine" part is really a
-per-*kind*-of-machine part.
-
-The predecessor produced these from one module with two *flavors*: a prompt flavor and an
-autocomplete flavor, each of which swapped in a different local file. Its install hook also:
-
-- cloned the prompt theme and three plugins from their upstream repositories into `~/.zsh/`;
-- installed fonts;
-- changed the login shell.
-
-On the workstations the theme and plugins are still on disk, left over and now owned by nothing. The
-servers have none of them.
-
-## What the 102 lines are
-
-Sorted by who should own each line once the machine is modules:
-
-| Lines today | What they are | Natural owner |
-|---|---|---|
-| `EDITOR`, `VISUAL`, `XDG_CONFIG_HOME`, `PATH` gaining `~/.local/bin` and two script directories | the account's environment | the shell's default, or the environment itself |
-| `PATH` gaining a toolchain's directory | environment, for one tool | the toolchain's module |
-| a version manager's directory variable plus sourcing its loader | environment *and* shell code | the version manager's module |
-| two variables naming the operator's own script library | environment, the operator's own | the operator |
-| a variable that turns off an agent's terminal-title handling | environment, for one tool | the agent's module |
-| the terminal title hook, keybindings, `dircolors`, the `ls`/`grep` aliases, `ll`/`la`/`l`, a container-run alias, two disk-usage functions, two port aliases | interactive shell behaviour | the shell's default |
-| the prompt's instant-prompt cache, the theme, the prompt's own configuration file | shell code, order-sensitive (instant prompt first) | the prompt module |
-| autosuggestions, syntax highlighting (and, unloaded, an autocomplete plugin on disk) | shell code, order-sensitive (syntax highlighting last) | a plugin module, or the prompt module |
-| sourcing `~/.zshrc.local` | the operator's hook | the operator |
-
-The workstation variant of the local file adds:
-
-- more environment: a desktop toolkit theme, a file manager's plugin list, `BROWSER`, `VISUAL`
- overridden to a graphical editor, a language toolchain's binary directory on `PATH`;
-- two pieces of shell code: one that pads the prompt to the bottom of the terminal under a display, and
- one that sources a function file another module places;
-- a hook sourcing a further per-node file.
-
-The server variant holds only that last module-placed source line.
-
-**Count:** a workstation runs 65 non-comment lines from the two files (53 shared, 12 local); a server
-runs 54. Of a workstation's 65:
-
-- about a quarter (15) are environment;
-- about half are interactive defaults no other module cares about;
-- the remaining quarter is other modules' code and hooks (a prompt, plugins, a version manager, an
- agent's functions), loaded from the shell file only because there was nowhere else to put it.
-
-## The shell module as written
-
-The `zsh` module of to-be 38 WP5 (catalogue change, unmerged):
-
-- writes one block, appended at the end of `~/.zshrc`, holding a subset of the shared file:
- - its environment lines, minus the toolchain directory, the version manager and the agent variable;
- - the title hook, keybindings and the most common aliases, minus the port aliases;
- - guarded `source` lines for the theme and two plugins *if present*;
- - the source of `~/.zshrc.local`.
-- assigned to any of the four machines, appends that block after the identical lines already there, so
- every line in it runs twice, `~/.zshrc.local` included.
-- on the servers, the guarded prompt lines find nothing; nothing installs the theme anywhere.
-
-## Which startup file reaches what
-
-zsh's startup order, and what each path through it reads:
-
-| started as | reads |
-|---|---|
-| interactive login (a console, ssh with a terminal) | `.zshenv`, `.zprofile`, `.zshrc`, `.zlogin` |
-| interactive non-login (a new terminal window) | `.zshenv`, `.zshrc` |
-| non-interactive login: `zsh -lc …`, what the `execute` verb runs | `.zshenv`, `.zprofile`, `.zlogin`, **not** `.zshrc` |
-| non-interactive: a script, `ssh host command` | `.zshenv` only |
-
-So an environment written into `.zshrc` reaches neither `execute` nor a script. The distribution's
-system-wide login profile, which zsh's system `zprofile` sources, only ever *appends* to `PATH` when an
-entry is missing. An entry the account's `.zshenv` puts first therefore survives a login.
-
-A graphical session's programs (a launcher, a bar, a window manager's key bindings) are started from the
-display manager and the service manager, not from a shell, and read none of these files. The service
-manager's own place for the account's environment is `~/.config/environment.d/`. Today it holds nothing
-on any of the four machines, so a program launched from the window manager does not see `PATH` entries
-that a terminal does.
-
-## What the mesh already has for "many modules, one file"
-
-Measured over the catalogue's 69 module definitions:
-
-| mechanism | used by | shape |
-|---|---|---|
-| `contributes` / `receives` | 28 contribute, 15 receive | A consumer contributes **facts** keyed by a requirement. The provider receives all of them as one file in the mesh's own format, and **renders them itself**. "The controller does not know what a reverse proxy is." |
-| `listens` / `filtering` | 40 declare listens, 1 composes | The controller derives the whole firewall rule set from every module's ports and writes it where the holder asks. |
-| `jails` / `jailing` | 3 declare, 1 composes | Each module supplies its jail **in the tool's own format**. The controller assembles them, sorted, into the one file the holder names. |
-| `into: block` on a file | 2 | One module's marked region inside a file something else owns. Text outside the region is kept byte for byte. Placement is at the end, or at the start. |
-
-None of these is a contribution of shell code or of environment today.
diff --git a/01-RESEARCH/025-how-a-module-plugs-into-the-shell/02-how-a-module-plugs-in.md b/01-RESEARCH/025-how-a-module-plugs-into-the-shell/02-how-a-module-plugs-in.md
deleted file mode 100644
index ad0f7945..00000000
--- a/01-RESEARCH/025-how-a-module-plugs-into-the-shell/02-how-a-module-plugs-in.md
+++ /dev/null
@@ -1,197 +0,0 @@
-# 02 — How a module plugs in
-
-Six questions, taken one at a time: the environment, shell code, the operator's own lines, order,
-who renders, and what a contribution is addressed to. Each has the options weighed and a starting
-position. The positions were set with the operator on 2026-10-04 and are what this effort tests, not
-what it has decided.
-
-## 1. The environment: variables and `PATH`
-
-A variable or a `PATH` entry is a fact about the account. It holds whichever shell is the login shell,
-and it is wanted by:
-
-- every shell, interactive or not;
-- the login shell's `execute`;
-- a graphical session's programs.
-
-[01](01-what-the-shell-file-holds-today.md) measures that `.zshrc` reaches only the first kind, and
-only interactively.
-
-| | option | for | against |
-|---|---|---|---|
-| E1 | Each module writes lines into the shell's rc file (today) | nothing new | misses `execute`, scripts and the graphical session; written in one shell's syntax, so a second shell module starts over |
-| E2 | A module contributes environment facts (a variable and its value; a `PATH` entry and its position) **to the login shell**. The holder renders them into its shell's always-read file (`~/.zshenv` for zsh) | reaches every zsh, `execute` included; a contributor names no path and no shell | the graphical session sees none of it; the environment is tied to which module holds the shell; every shell module reimplements the same rendering |
-| E3 | E2, and the service-manager holder (ADR 0177) renders the same facts a second time into `~/.config/environment.d/` | the graphical session sees the same `PATH` as the terminal | one fact set, two owners, two renderings that can disagree; the service manager's module gains a duty unrelated to managing services |
-| E4 | One composed file in `environment.d` syntax, sourced by the shell with export-all | one file, two readers | ties the shell to the service manager's syntax, which is close to POSIX assignments but not equal (its `${VAR:-default}` and quoting rules differ); a value with a space breaks one reader or the other |
-| E5 | Shells take the environment from the service manager's environment generator, which prints the merged `environment.d` | no file of the shell's at all | every shell depends on the service manager and starts a process on every start; the generator's output is unquoted, so a value with a space breaks it |
-| **E6** | **An environment module.** A module of its own (working name `node-env`) holds a mesh seat, `node-environment`, and is the only writer of the account's environment. Every module contributes its variables and `PATH` entries to that seat. The holder writes them in each reader's format: a POSIX file of `export` lines that shells source, and the service manager's `~/.config/environment.d/` | the environment no longer depends on which shell holds `login-shell`; one owner and one rendering per format, both from the same facts; the graphical session included without the service manager's module; a contributor addresses "the environment", never a shell; the `PATH` rules (order, de-duplication) live in one module's code, where a test can hold them | one more module and seat, assigned on every node beside the shell; the `login-shell` protocol gains a duty, to source the environment file, which must be written down and checked |
-
-**Starting position: E6.** It was the operator's proposal on 2026-10-04, and it replaces this
-document's first position (E2, then E3).
-
-- The facts are the contribution. Each format is rendered once, by the one module whose subject is the
- environment.
-- A shell module's part shrinks to one line in its always-read file: `.zshenv` for zsh, sourcing the
- environment module's POSIX file. A bash or fish module writes the same line in its own file, and no
- contributor changes when the login shell does.
-
-Sketched, for a node with zsh, the environment module, and a toolchain:
-
-```
-toolchain ──contributes PATH entry──▶ node-environment ◀──contributes EDITOR, ~/.local/bin── zsh
- │ (held by node-env)
- ┌──────────────────┴──────────────────┐
- ▼ ▼
- POSIX export file ~/.config/environment.d/
- ▲ ▲
- sourced from ~/.zshenv read by the service manager
- (every zsh, execute too) (the graphical session)
-```
-
-The shell module still contributes its own environment (`EDITOR`, `XDG_CONFIG_HOME`, `~/.local/bin` on
-`PATH`) as a contributor like any other; it does not write those lines itself. Once issue 168 closes,
-the values a person varies become settings of whichever module contributes them (ADR 0174).
-
-## 2. Shell code: a prompt, plugins, a version manager's loader
-
-This *is* one shell's syntax, and order matters: a prompt's instant-prompt cache must run first, and
-syntax highlighting last.
-
-| | option | for | against |
-|---|---|---|---|
-| S1 | **A contribution of code for one shell** (the shell it is for, the code, a slot), gathered by the controller and placed inside the holder's block in slot order. The same shape as `jails`, which a module supplies in fail2ban's own format and the controller assembles | a contributor names no path; the order is declared and checkable; unassigning the contributor removes its code at the next composition; a node holding fish simply has no zsh code rendered, and the resolver can say so | the controller gains one more gathered field; code for a shell travels in the declaration (in the clear, so no secrets in it, as for any file) |
-| S2 | **A drop-in directory**: each module places its own `~/.zsh/rc.d/NN-name.zsh`, and the shell's block sources the directory | no controller change; each file is its module's own, removed when undeclared | every contributor hard-codes a path inside the shell module's territory, against ADR 0112's spirit; order is a naming convention nothing checks; nothing ties the file to the shell actually being zsh |
-| S3 | Contributions as facts the holder renders (`contributes`/`receives` proper) | one mechanism with question 1 | code is not a fact; the holder would only paste it, which is S1 with an extra file |
-
-**Starting position: S1.** A contribution to the shell carries **only code**, for named shells, each
-piece in a slot. Variables and `PATH` entries never go here; they go to the environment (§1). So a
-module touching both makes two contributions:
-
-- A prompt module contributes zsh code in the first slot, and its own configuration file is its own
- owned file (ADR 0182).
-- A version manager contributes its directory variable to the environment, and its loader as code
- for each shell it supports.
-- A toolchain contributes a `PATH` entry to the environment and nothing to the shell.
-
-What has to be settled: what each contribution is *addressed to*. Section 6 covers that.
-
-## 3. The operator's own lines: the "local override"
-
-Assigning the shell module must lose nothing the machine does today. That has two halves.
-
-**What is common is the module's default, not an override.** The startup file is identical on all four
-machines ([01](01-what-the-shell-file-holds-today.md)). A line every machine has is the shell module's
-default, or another module's contribution. It is not a local override that a person would keep in step
-on every machine by hand. Most of today's file therefore moves into the shell module's block and into
-the contributions above. Little of it stays the operator's.
-
-**What is the operator's is everything outside the mesh's block.** The host already works this way:
-
-- the mesh's region is the marked block;
-- text outside it is kept byte for byte, and checked unchanged;
-- the region is given back when the module goes.
-
-| | option | for | against |
-|---|---|---|---|
-| O1 | The mesh's block at the **start** of the file; the operator's lines after it | the operator's lines run last and win, which is what an override means; already supported (`at: start`) | a file the operator later rewrites must keep the markers; the host refuses a broken pair rather than guess |
-| O2 | A named operator region *inside* a file the mesh writes whole (ADR 0174's wording) | the file is entirely the mesh's except one hole | the opposite of what the host implements; a file a person already owns becomes the mesh's |
-| O3 | Only `~/.zshrc.local`, sourced from the block; `~/.zshrc` the mesh's whole | one obvious place | takes over a file the person owns today; ADR 0182 classifies the shell's own file as *written into*, not owned |
-
-**Starting position: O1.** `~/.zshrc.local` keeps working because the operator's own lines source it,
-not because the mesh's block does.
-
-The record this effort becomes corrects ADR 0174's description of the kept region as a **progressive
-insight**: the decision stands (a node varies a module by settings or by the operator's own lines,
-never by an edit), and only its description of which side is marked changes.
-
-**The one-off migration** is a person's act, listed in the module's documentation (ADR 0182):
-
-- remove from today's file every line the block or a contribution now carries;
-- keep the rest below the block.
-
-Until a prompt module and the other contributors exist, the lines they will carry stay among the
-operator's own. Nothing is lost at any step.
-
-## 4. Order
-
-Order matters only for code. The environment is set before any code runs, because zsh reads
-`.zshenv` first. `PATH` entries carry their own position (before or after the system's), which the
-environment module orders, not the shell.
-
-| | option | for | against |
-|---|---|---|---|
-| R1 | Numbers (`10`, `50`, `90`) | familiar | every contributor guesses a number; collisions are silent |
-| R2 | **A few named slots**, `first` / `normal` / `last`, with the module name breaking ties | the prompt says `first` and highlighting says `last` because that is what they mean; the composed result is the same bytes every time | three slots may not be enough |
-
-**Starting position: R2.** Inside the shell module's block, the order is:
-
-1. the line sourcing the environment module's file (in `.zshenv`, so it runs for every zsh; the rest
- of this list is `.zshrc`);
-2. the `first` slot;
-3. the shell module's own defaults;
-4. the `normal` slot;
-5. the `last` slot.
-
-The operator's lines come after the block, as option O1 says.
-
-## 5. Who renders: the controller or the holder's code
-
-There are two different renderings, and E6 lets them be answered differently.
-
-**The environment** is facts rendered into two fixed formats by the one module whose subject they are.
-
-- The environment module receives the gathered contributions (the `contributes` / `receives` shape:
- facts in the mesh's own format, rendered by the receiver).
-- Its own code writes the POSIX file and the `environment.d` file whenever what it receives changes.
- That is ADR 0182's third class, written by the module's own process, owned by the account,
- atomically.
-- The controller learns no shell and no service manager. The `PATH` rules (prepend or append,
- de-duplicate, keep the system's entries) are ordinary code with ordinary tests.
-- To settle: what runs that code when the received file changes. The candidates are a host action
- that restarts on the received file, or a subscription through the runtime (ADR 0198).
-
-**Shell code** is not facts. It is text in the shell's own syntax, assembled in slot order, which is
-what the controller already does for fail2ban jails: sort the pieces and concatenate them into the
-holder's region. The controller assembles; it never interprets the code. This keeps the shell module
-bundle-free for its files, and keeps the composed result visible in the declaration before a machine
-applies it.
-
-## 6. What a contribution is addressed to
-
-Under E6 there are two addressees: the environment and the login shell.
-
-| | option | for | against |
-|---|---|---|---|
-| A1 | **Seats**: environment facts to `node-environment`, shell code to `login-shell`. Each seat's protocol says what its holder does with what is contributed to it | a contributor depends on a role ("the environment", "the login shell"), never on zsh or on one module; works the same for any holder | `login-shell` today is declared by the zsh module itself (ADR 0126). A second shell module may only claim it, never declare it, and the seat exists only while zsh's definition is registered |
-| A2 | Requirements the modules provide (`contributes` keyed by them, as the reverse proxy is) | an existing mechanism | a contributor on a node without the provider fails to resolve, though a toolchain's `PATH` entry with no environment module is merely unwritten |
-
-**Starting position: A1, both seats in the mesh's own seat set** beside the service manager.
-
-- `node-environment` is new, and is the mesh's from the start.
-- `login-shell` moves there from the zsh module's definition. A shell is as universal a role as a
- service manager, and a protocol that now carries duties (render the shell code contributed to it,
- source the environment file) should not depend on one module's registration.
-
-Research 023 (a seat's protocol naming what its holder owns) is the general form of this: the
-environment seat would own the two environment files, and the login-shell seat the shell's
-startup-file region. The two efforts should not decide it twice.
-
-## Open questions
-
-- Whether a contribution may be conditional on a capability: the workstation-only environment (a
- browser, a toolkit theme) is a desktop module's contribution, which arrives only where that module is
- assigned. Measured, this may need nothing new.
-- What a node without the environment module does with environment contributions: refuse them at
- resolve, or leave them unwritten and say so. The position here is to say so; a missing `PATH` entry
- is a visible gap, not a broken machine.
-- Whether the operator's own variables (the script-library paths in [01](01-what-the-shell-file-holds-today.md))
- are the operator's lines below the shell block, or a kept region of the environment module's file.
- The first needs nothing new, but reaches only interactive zsh.
-- How the prompt module and a plugin module divide the plugins. Packaging decides it as much as
- ownership: the plugins come from upstream repositories, not distribution packages, on these machines.
-- Whether the `execute` verb should read the interactive file at all once the environment is in
- `.zshenv`. The position here is no: a non-interactive login shell plus the environment is what a
- command needs, and the prompt's code should not run for it.
-- How a contribution reaches a second shell assigned beside the holder, which to-be 38 WP5 names as the
- first follow-up record. Under A1 a non-holder renders nothing, so the question becomes whether a
- non-holding shell module may render contributions for interactive use.
diff --git a/01-RESEARCH/026-the-graphical-session-as-modules/00-overview.md b/01-RESEARCH/026-the-graphical-session-as-modules/00-overview.md
deleted file mode 100644
index 4f93b5d6..00000000
--- a/01-RESEARCH/026-the-graphical-session-as-modules/00-overview.md
+++ /dev/null
@@ -1,91 +0,0 @@
----
-status: graduated
-initiated: 2026-10-04
-touches:
- - 02-DECISIONS/0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md
- - 02-DECISIONS/0177-a-unit-may-be-user-scoped-and-the-service-manager-is-a-node-seat.md
- - 02-DECISIONS/0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md
- - 02-DECISIONS/0203-the-accounts-environment-is-one-modules-and-every-module-contributes-to-it.md
- - 02-DECISIONS/0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md
- - 02-DECISIONS/0205-software-the-distribution-does-not-package-ships-as-a-pinned-archive-of-the-module.md
- - 03-DESIGN/01-to-be/37-the-operators-machine.md
- - 03-DESIGN/01-to-be/38-building-the-operators-machine.md
- - 04-ISSUES/168-a-setting-reaches-every-file-and-contribution/00-report.md
-became:
- - 02-DECISIONS/0208-the-graphical-session-is-one-module-per-piece-on-the-meshs-seats.md
- - 02-DECISIONS/0210-a-tools-configuration-is-its-seat-holders-and-every-other-module-extends-it-through-the-seat.md
- - 02-DECISIONS/0211-a-machines-power-is-a-node-seat-its-moments-take-contributions-and-its-states-are-events.md
- - 02-DECISIONS/0212-a-seat-says-what-it-receives-and-the-machines-hotkeys-are-a-seat.md
- - 02-DECISIONS/0207-a-module-depends-on-the-node-seats-that-apply-its-resources.md
- - 02-DECISIONS/0215-the-machines-message-bus-is-a-node-seat-and-is-never-restarted-live.md
- - 02-DECISIONS/0255-the-bar-takes-blocks-as-data-from-the-module-that-knows-the-hardware-placed-where-the-machine-has-it.md
- - 02-DECISIONS/0260-a-bar-block-shows-a-value-its-module-keeps-on-the-bus-and-no-module-reads-anothers-files.md
- - 03-DESIGN/01-to-be/42-the-machines-modules-in-order.md
----
-
-# 026 — The graphical session as modules
-
-## What is investigated
-
-The workstations' graphical session as modules of the mesh, at the same level as the shell
-([to-be 41](../../03-DESIGN/01-to-be/41-the-shell-and-the-accounts-environment.md)): a package, files
-under the account's home, a seat, and nothing that names a machine. The pieces are:
-
-- the login manager;
-- how a session starts and what environment it gets;
-- the display server (X today, Wayland as a sibling);
-- the window manager (i3, and sway as its Wayland sibling);
-- the terminal emulator (xterm);
-- the session's companions: bar, compositor, launcher, notifier, lock and idle, clipboard,
- wallpaper, theming, fonts.
-
-[To-be 38](../../03-DESIGN/01-to-be/38-building-the-operators-machine.md) names this WP7, and says
-each seat begins with a record naming its holders and verbs. [To-be 37](../../03-DESIGN/01-to-be/37-the-operators-machine.md)
-§4 leaves one question for the resolver: whether a held seat can gate another's assignment.
-
-## Why
-
-The operator asked for the graphical modules next, at the shell's level, and for one consistent
-experience across machines. Since the predecessor retired, nothing manages the workstations'
-desktops. Measured in [01](01-what-the-workstations-run.md):
-
-- Two workstations carry one 983-line predecessor module's output, still byte-identical in its core.
-- One workstation also carries another machine's hardware fragments.
-- One runs a session that predates two fixes, with two notification daemons and two portals.
-- The session's environment is a hand-kept second copy of the account's, beside the one the mesh
- now writes.
-
-## How it is approached
-
-**Adopting is also improving** (the operator, 2026-10-04). A module is not a copy of what a machine
-does today. Making it is the moment to fix what is broken, drop what is dead, choose the better tool
-and remove the leftovers. Every module's design lists its improvements over today. **Every module
-also serves tools,** many of them, for reading, acting and diagnosing; a module that only places a
-package and a file is unfinished. The tools are catalogued in
-[026/05](../026-the-graphical-session-as-modules/05-the-tools-each-module-serves.md).
-
-## What it touches
-
-- **The seat table:** up to ten node seats.
-- **The resolver:** a seat held on a node gating another module's assignment.
-- **The contribution mechanism of ADR 0204:** whether it generalises beyond shells, or whether
- tools' own drop-in directories serve.
-- **The host's user-scoped units** (mesh-host #72, still open).
-- **Settings** for per-machine values (issue 168).
-- **ADR 0205's archive** for the two pieces the distribution does not package.
-
-## Documents
-
-- [01 — What the workstations run](01-what-the-workstations-run.md): evidence.
-- [02 — The questions and the options](02-the-questions-and-the-options.md)
-- [04 — Screensaver, displays and menus](04-screensaver-displays-and-menus.md): the lock and idle module, monitor layouts by the monitors' identity, rofi and dmenu behind one launcher seat, the clipboard, fonts
-- [05 — The tools each module serves](05-the-tools-each-module-serves.md): a first catalogue for the modules of 026 and 027
-- [03 — What the predecessor taught](03-what-the-predecessor-taught.md): its 128 modules and 3,395 commits, as patterns to keep and failures not to repeat; shared with research 027.
-
-## What it became
-
-*Closed 2026-10-10.* The graphical session is one module per piece on the mesh's seats ([ADR 0208](../../02-DECISIONS/0208-the-graphical-session-is-one-module-per-piece-on-the-meshs-seats.md)): a tool's configuration
-is its seat holder's ([ADR 0210](../../02-DECISIONS/0210-a-tools-configuration-is-its-seat-holders-and-every-other-module-extends-it-through-the-seat.md)), the hotkeys and the power are seats ([ADR 0212](../../02-DECISIONS/0212-a-seat-says-what-it-receives-and-the-machines-hotkeys-are-a-seat.md), [ADR 0211](../../02-DECISIONS/0211-a-machines-power-is-a-node-seat-its-moments-take-contributions-and-its-states-are-events.md)), the
-machine's message bus is a seat never restarted live ([ADR 0215](../../02-DECISIONS/0215-the-machines-message-bus-is-a-node-seat-and-is-never-restarted-live.md)), and the bar takes blocks as data from
-the bus ([ADR 0255](../../02-DECISIONS/0255-the-bar-takes-blocks-as-data-from-the-module-that-knows-the-hardware-placed-where-the-machine-has-it.md), [ADR 0260](../../02-DECISIONS/0260-a-bar-block-shows-a-value-its-module-keeps-on-the-bus-and-no-module-reads-anothers-files.md)). A module depends on the node seats that apply its
-resources ([ADR 0207](../../02-DECISIONS/0207-a-module-depends-on-the-node-seats-that-apply-its-resources.md)), which answers whether a held seat gates another module. The order of building and rolling out is [to-be 42](../../03-DESIGN/01-to-be/42-the-machines-modules-in-order.md).
diff --git a/01-RESEARCH/026-the-graphical-session-as-modules/01-what-the-workstations-run.md b/01-RESEARCH/026-the-graphical-session-as-modules/01-what-the-workstations-run.md
deleted file mode 100644
index c9317c89..00000000
--- a/01-RESEARCH/026-the-graphical-session-as-modules/01-what-the-workstations-run.md
+++ /dev/null
@@ -1,141 +0,0 @@
-# 01 — What the workstations run
-
-Measured 2026-10-04 on the two workstations of one installation, read-only: a laptop with a hybrid
-GPU and an internal panel, and a desktop with one GPU and two external monitors. Both run the same
-predecessor-generated desktop. File equality was checked by checksum across the two machines.
-
-## How a session starts
-
-The chain is the same on both:
-
-1. The login manager (`lemurs`, built from the distribution's user repository, its package now in
- the official one) runs its X setup script on a virtual terminal.
-2. That script sources the login shell's profile files, then `~/.xprofile`, then the system's
- `xinitrc.d` drop-ins, then merges `~/.Xresources`.
-3. `~/.xprofile` reuses the systemd user manager's bus, then sources `~/.xinitrc`.
-4. `~/.xinitrc` sets up the session and ends with `exec i3`.
-
-The login manager's own window-manager entry (`exec startx`) is never reached. Its configuration
-file uses a format two releases old, and an unmerged newer one sits beside it.
-
-**What `~/.xinitrc` does**, in order:
-
-1. Sources the system drop-ins, which import `DISPLAY` and `XAUTHORITY` into the user manager.
-2. Starts the keyring and exports its ssh socket.
-3. Exports the session's environment:
- - `PATH`, with nine entries, one of them a directory that no longer exists;
- - toolchain variables;
- - `XDG_CONFIG_HOME` and `XDG_DATA_DIRS` (with flatpak);
- - five GTK/Qt theme variables;
- - the desktop's identity (`XDG_CURRENT_DESKTOP`, `XDG_SESSION_DESKTOP`);
- - three of the operator's own variables.
-4. Imports an explicit allowlist of ten of those into the user manager and D-Bus activation. It is
- never `--all`, because:
-5. a predecessor file of **secrets as environment variables** (package-registry and API tokens) is
- sourced next.
-6. Sets the screensaver and display power timeouts, restores the wallpaper, and starts the lock
- watcher in a respawn loop. It is deliberately not a unit, because it needs the login session.
-7. `exec i3`.
-
-**The account's environment, as of today, has three sources that disagree:**
-
-- this file, for the session;
-- the mesh's `environment.sh`, for shells
- ([ADR 0203](../../02-DECISIONS/0203-the-accounts-environment-is-one-modules-and-every-module-contributes-to-it.md));
-- `~/.config/environment.d/`, for the user manager. It holds the mesh's `50-mesh.conf`, and a
- predecessor file that **sets `PATH` outright** and sorts after it.
-
-## The roles, and what fills them
-
-| role | software | where configured |
-|---|---|---|
-| login manager | lemurs | `/etc/lemurs/*` (identical on both, and to the predecessor's source) |
-| session start and environment | the login manager's X setup, `~/.xprofile`, `~/.xinitrc`, `xinitrc.d`, the D-Bus import, `environment.d` | `~/.xprofile`, `~/.xinitrc`, `~/.config/environment.d/*` |
-| display server | Xorg (`xorg-server`, `xinit`, the X apps; vendor drivers per GPU) | **no** `xorg.conf.d`; monitors by `xrandr` scripts |
-| monitor layout | `xrandr` scripts (arandr), a hotplug rule on the laptop | `~/.screenlayout/`, a scripts folder, a window-manager fragment |
-| window manager | i3 4.25 | `~/.config/i3/config` and `config.d/*`, a reload watcher (user unit) |
-| bar | i3bar with i3status-rust | `~/.config/i3status-rust/*`, 14 themes, a bar watchdog (user unit) |
-| terminal | xterm (the only terminal installed) | `~/.Xresources.d/xterm`, the window manager's binding, the compositor's opacity rule |
-| compositor | picom | `~/.config/picom/picom.conf` |
-| launcher and menus | rofi | `~/.config/rofi/*`, launcher, power-menu and theme-picker scripts |
-| notifier | dunst (D-Bus activated) | `~/.config/dunst/dunstrc`, `dunstrc.d/*` |
-| lock, idle, display power | xss-lock and i3lock-color, `xset` | `~/.xinitrc`, a lock script |
-| clipboard | greenclip, xclip | `greenclip.toml` |
-| wallpaper | feh | `~/.fehbg` (points into the predecessor's tree) |
-| theming | Adwaita dark, qt5ct/qt6ct, the desktop portal (GTK backend pinned) | GTK `settings.ini`, `qt*ct.conf`, `portals.conf`, an appearance script, `.Xresources` cursor |
-| fonts | Hack and Meslo Nerd fonts in `~/.local/share/fonts` (not packaged), noto | `~/.Xresources.d/xft` (DPI fixed at 96) |
-| keyboard | nothing set; the default layout; vendor keys via triggerhappy on the laptop | window-manager bindings, `/etc/triggerhappy` |
-
-**Packages:** every piece except two is in the distribution's official repositories, and the login
-manager now is too. The two exceptions are the lock screen's colour build (`i3lock-color`) and the
-clipboard manager (`rofi-greenclip`). The Nerd fonts exist as official packages, but both machines
-carry hand-copied files instead.
-
-## Identical, different, and why
-
-**Byte-identical on both machines:**
-
-- the session files: `.xinitrc`, `.xprofile`, `.Xresources` and its drop-ins;
-- the i3 main configuration and two of its fragments;
-- the bar's top configuration and themes;
-- picom, rofi, the GTK and Qt settings, the portal configuration, the login manager.
-
-**Different, by cause:**
-
-| cause | what |
-|---|---|
-| hardware | the monitor layout script; the bar's battery block; the laptop's power and vendor-key units and udev rules |
-| misassignment | the desktop carries the **laptop's** hardware fragments: the vendor-key daemon and its triggers, the backlight rule, the brightness drop-in, a touchpad reset, and the laptop's monitor layouts, in an older version |
-| drift | the notifier's position and corner radius; a "temporary" window-manager fragment from a test; the bar watchdog disabled; a second Qt configuration tool; different font builds |
-| a stale session | the desktop's session began before two fixes, so it runs two notification daemons and two portals, and its user manager lacks the desktop's identity |
-
-**Dead references:** the window manager starts a polkit agent that is installed on neither machine,
-so there is no polkit agent at all. `PATH` names a directory that does not exist.
-
-**Per-machine values inside shared files:**
-
-- the DPI;
-- absolute home paths, in the clipboard configuration and the flatpak data directories;
-- the laptop's panel name, inside a fragment both machines carry.
-
-## User units the desktop needs
-
-| unit | does | laptop | desktop |
-|---|---|---|---|
-| reload watcher | reloads the window manager and bar when their files change | on | on |
-| bar watchdog | restarts a dead bar | on | off |
-| clipboard daemon | from its package | via the window manager | unit **and** window manager |
-| vendor power profile, memory guard | laptop power | on | — |
-
-None is managed. Applying them as the account needs the host's user scope
-([ADR 0177](../../02-DECISIONS/0177-a-unit-may-be-user-scoped-and-the-service-manager-is-a-node-seat.md)),
-which is still an open change.
-
-## The predecessor's module
-
-One manifest of 983 lines covers the window manager, bar, launcher, notifier, compositor, lock
-screen, session bootstrap, theming and scripts. It:
-
-- has four *flavors*: i3, laptop (i3 plus the monitor wizard and hotplug), desktop (i3 plus
- nothing) and a laptop model (laptop plus vendor keys);
-- has about **105 theme variables** substituted into templates: border, gaps, fonts, workspace
- names, every colour of bar, launcher, notifier and lock screen, compositor opacity, cursor, idle
- times, Qt and GTK theme names;
-- enables the two user units from an install hook.
-
-Separate modules held the login manager and the display server (one flavor, `xorg`, with a comment
-calling `wayland` "the intended sibling"). The shell module held no graphical part.
-
-## Wayland and sway
-
-**Nothing exists.** There is no compositor, no sway configuration, no Wayland session entry, and the
-login manager's Wayland directory is empty. What is installed is libraries:
-
-- Wayland itself and the Qt Wayland plugins, which other packages pull in;
-- `xwayland`, explicitly installed and required by nothing;
-- on the desktop, an orphaned compositor library from another desktop environment, and that
- environment's portal backend, pulled in by a game launcher. The portal configuration pins
- against it.
-
-Every piece a sway session needs is in the official repositories: the compositor, its lock screen,
-a terminal (`foot`), a bar (`waybar`), a notifier (`mako`) and `xwayland`.
diff --git a/01-RESEARCH/026-the-graphical-session-as-modules/02-the-questions-and-the-options.md b/01-RESEARCH/026-the-graphical-session-as-modules/02-the-questions-and-the-options.md
deleted file mode 100644
index 7600ac63..00000000
--- a/01-RESEARCH/026-the-graphical-session-as-modules/02-the-questions-and-the-options.md
+++ /dev/null
@@ -1,123 +0,0 @@
-# 02 — The questions and the options
-
-Seven questions. Each has its options and a starting position, which is what this effort tests, not
-what it has decided.
-
-## 1. How finely the desktop splits into modules
-
-| | option | for | against |
-|---|---|---|---|
-| G1 | One desktop module, as the predecessor had | one assignment | flavors again, per machine; ADR 0174 refuses them, and the evidence shows a flavor landing on the wrong machine |
-| G2 | **One module per piece of software:** `lemurs`, `xorg`, `i3`, `i3status-rust`, `xterm`, `picom`, `rofi`, `dunst`, `xss-lock` with the lock screen, `greenclip`, `feh`, a theme module, a fonts module | each is what it declares; a machine gets exactly what is assigned; the same split already works for the shell and its plugins | about thirteen assignments per workstation |
-| G3 | G2, plus a named **set** the controller assigns as one (for example *the X desktop*) | G2's precision with G1's convenience | a set is a new controller concept |
-
-**Starting position: G2.** Whether a set is worth a record is left until the thirteen assignments
-have been done by hand once.
-
-## 2. The seats
-
-Research 018 listed the candidates. ADR 0204 has since put the login shell in the mesh's own set,
-because a role with a protocol should not depend on one module's registration. The same reasoning
-applies here:
-
-| seat | holders | protocol, first verbs |
-|---|---|---|
-| `node-login-manager` | lemurs, greetd | which sessions it offers, the default session |
-| `node-display-server` | xorg, sway | `displays`, `layout` |
-| `node-display-session` | i3, sway | `reload`, `workspaces`, `windows` |
-| `node-terminal-emulator` | xterm, foot, alacritty | which terminal `$TERMINAL` names; `open` |
-| `node-bar`, `node-compositor`, `node-launcher`, `node-notifier`, `node-lock-screen`, `node-clipboard` | the pieces above, and their Wayland counterparts | one verb or none each, until a use asks for one |
-
-**A compositor that is its own server holds two seats.** Sway is both the display server and the
-display session. A module may claim several seats, so this needs nothing new.
-
-**Starting position:** the first four seats are in the mesh's own set. The companion seats are
-added only as each holder is written; for those, a module without a seat is acceptable at first.
-
-## 3. One module requiring another seat to be held
-
-i3 needs an X server held on its node, and sway needs nothing below it. A terminal needs a session.
-To-be 37 left open how that is said.
-
-| | option | for | against |
-|---|---|---|---|
-| R1 | A seat **delivers a provision** (`x11-display`, `wayland-display`) and a module requires it at node scope. The seat table has a `delivers` field already, and requirements already resolve | existing machinery; the refusal names the seat and its possible holders, which design 27 already lists | a node-scoped requirement that never crosses machines has to be stated as such |
-| R2 | A new field, *needs the seat X held* | reads plainly | a second way to say what R1 says |
-| R3 | Nothing; assign carefully | — | the mistake the evidence shows (a laptop's fragments on a desktop) is exactly an unchecked assignment |
-
-**Starting position: R1.** `xorg` and `sway` each deliver what they serve. `i3`, `picom` and `xss-lock`
-require `x11-display`. `foot` requires a Wayland display, and xterm requires an X one, which a Wayland
-session gives through `xwayland`.
-
-## 4. Who starts the session, and with what environment
-
-Today `~/.xinitrc` is a hand-kept second environment and the session's whole start script.
-
-| | option | for | against |
-|---|---|---|---|
-| S1 | The display server's module writes `~/.xinitrc` **into**: a mesh block at the start that sources the account's environment (`environment.sh`), merges the X resources, and runs the session's contributed start lines. The session holder's module contributes its `exec` line. The operator's lines stay after the block | one environment for shells, the session and the user manager; nothing to keep in step | the order inside `.xinitrc` becomes the slot order of a contribution (question 5) |
-| S2 | The login manager's module owns the session script under `/etc` | system scope; no home file | the environment is the account's, and the script is the same for every account |
-| S3 | Leave `.xinitrc` the operator's | nothing to build | the third environment stays |
-
-**Starting position: S1.**
-
-- The desktop's identity (`XDG_CURRENT_DESKTOP`) and the theme variables become **environment
- contributions** (ADR 0203) from `i3` and from the theme module. They then also reach the user
- manager through `environment.d`, which replaces most of today's allowlist import.
-- The secrets file stays out of the environment until research 027 settles how a secret reaches an
- account.
-
-## 5. How other modules contribute to a holder's file
-
-The terminal's settings are X resources. A bar, a launcher binding and a hardware module's key
-bindings are window-manager configuration. Autostarts are the session's. ADR 0204 built slot
-contributions for shells only.
-
-| | option | for | against |
-|---|---|---|---|
-| C1 | **The tool's own drop-in directory**, where it has one: i3's `include`, dunst's `dunstrc.d`, X resources' `#include`, XDG autostart entries, `environment.d`. Each contributor owns its own file there | no mesh change; the tools already read these directories; unassigning removes the file | each contributor names a path in another tool's directory (ADR 0204 rejected this for shells, where no drop-in convention exists); ordering is by file name |
-| C2 | **ADR 0204's mechanism generalised:** `contributes` text *for a format* (`zsh`, `xresources`, `i3`, `xinitrc`) in a slot, placed by the holder's placeholder | one mechanism, checked by the controller, order declared | every format must be named in the controller; a bigger change to ADR 0204 |
-| C3 | C1 where the tool has a drop-in convention, C2 where it does not (`.xinitrc`, `.Xresources` order) | uses each tool's own grain | two mechanisms to learn |
-
-**Starting position: C3**, with the boundary drawn by the tools. A tool that reads a directory gets
-drop-ins. A file without one gets slots. This means amending ADR 0204's "shell" to "a format", which
-is a progressive extension rather than a reversal.
-
-## 6. What varies per machine
-
-| what | today | option |
-|---|---|---|
-| monitor layout | per-machine `xrandr` scripts, monitor names baked in | a **setting** of `xorg` (issue 168), and a `layout` verb of the display server seat |
-| DPI, fonts' size | fixed in an X resource | a setting |
-| battery block, vendor keys, brightness, touchpad | a laptop model's flavor | **a hardware module** per machine model, contributing its window-manager fragment, bar block and udev rules. The desktop simply is not assigned it |
-| theme (the 105 variables) | template substitution | settings of each tool's module, after issue 168 closes (ADR 0174). Until then each module carries today's values as its default |
-
-**Starting position:**
-
-- Hardware modules for what follows the machine.
-- Defaults now, settings after issue 168, for what the operator varies.
-- The monitor layout waits for settings. Until then it is an operator-owned script the display
- server's block calls if present.
-
-## 7. Wayland and sway
-
-Nothing of a Wayland session exists, and every piece is officially packaged. "Wayland" is a protocol,
-not a piece of software, so it has no module of its own. Its parts are `sway` (server and session),
-`swaylock`, `foot`, `waybar`, `mako`, and `xwayland` for X clients.
-
-**Starting position:**
-
-- The seats and the requirements (questions 2 and 3) are designed so that sway fits from the first
- day.
-- The X stack is built first, because it is what runs.
-- `sway` and its companions are written after that, and proven on one workstation as a second
- session the login manager offers beside i3. That lets the operator try it without losing the
- working desktop.
-
-## Prerequisites this effort cannot remove
-
-- **User-scoped units** (mesh-host #72) for the reload watcher and the bar watchdog.
-- **Settings** (issue 168) for monitors and theme values.
-- **The two packages not in the official repositories:** the lock screen's colour build and the
- clipboard manager. Each is ADR 0205's case, a pinned archive, or a choice of an official
- alternative (`i3lock` without colours; `clipmenu`/`cliphist`).
diff --git a/01-RESEARCH/026-the-graphical-session-as-modules/03-what-the-predecessor-taught.md b/01-RESEARCH/026-the-graphical-session-as-modules/03-what-the-predecessor-taught.md
deleted file mode 100644
index d4e41bef..00000000
--- a/01-RESEARCH/026-the-graphical-session-as-modules/03-what-the-predecessor-taught.md
+++ /dev/null
@@ -1,51 +0,0 @@
-# 03 — What the predecessor taught
-
-A study on 2026-10-04 of the retired predecessor:
-
-- its 128 module manifests, their hooks, its installer and its sync engine;
-- 3,395 commits of history;
-- what it left on four machines.
-
-This document holds what bears on the graphical session and on the system layer
-([research 027](../027-the-system-layer-as-modules/00-overview.md)). The evidence is in the
-predecessor's history. A commit is cited here by what it fixed, not by its hash, because the
-repository is private.
-
-## Keep: what worked
-
-| pattern | where it shows | in the mesh |
-|---|---|---|
-| ownership marked inside the file: inside the markers is reconciled, outside is kept verbatim | a block marker in a shared file, after an engine that rewrote whole files | kept regions (ADR 0174, ADR 0204) |
-| two writers get two files and an `include`, the include first | the ssh client's configuration, after two writers fought over one file | ADR 0203's two files; research 026 C1 |
-| one writer per file, one authority per action | only the reload watcher restarts the window manager, after three mechanisms each did | ADR 0182 |
-| refuse to write when the source of truth is unreadable; never empty a block because a query found nothing | a block of names was emptied by a failed query | — keep |
-| an unresolved template variable fails the install | a literal unfilled path was installed green | [issue 231](../../04-ISSUES/231-a-misspelled-placeholder-is-written-out-as-text/00-report.md): the mesh still has this gap |
-| prune only what you can prove you placed | stale files from earlier deliveries | ADR 0189 |
-| ensuring never rotates a credential | a silent rotation caused a retry storm, a ban of the shared address and a lost registry | ADR 0114 |
-| vendor only the files you use; never clone and link | three files instead of 77 MB | ADR 0205 |
-| copy, never symlink | a recursive delete followed a link, and every reinstall failed | ADR 0012 |
-| verification says what it did not check | a verifier said *clean* while the secret was still on disk | — keep |
-| alert once per condition | 411 alerts hid a 28-hour outage | ADR 0090 |
-
-## Do not repeat
-
-| failure | what it did | the mesh instead | where the mesh is still exposed |
-|---|---|---|---|
-| **Flavors** | variant files and packages per machine type: a gate dropped, the first-seen variant won, packages never installed, the verifier ignored the gate. On the day of the study a desktop carried a laptop model's fragments | one module per piece, assignment per machine (ADR 0174, research 026 §1) | a setting that switches which whole file is rendered is a flavor under another name |
-| **The freeze** | existing values outranked new defaults; templated files were rendered once (*copy if absent*) | files are generated (ADR 0011) | a created-once file (ADR 0087) is a deliberate freeze, and a push must say *kept* |
-| **Adopting drift** | a *merge* strategy made a local edit the record forever; switching strategies clobbered a person's model choice | nothing is read back (ADR 0174) | an edit outside a kept region is overwritten **silently**. The predecessor's *why is this back* loop: the push should name what it overwrote |
-| **Environment templating** | `${VAR}` matched any name; unresolved names stayed literal; comments and destination paths were interpolated | namespaced placeholders; `$` forbidden in contributed values (ADR 0203) | issue 231 |
-| **Hooks with privilege** | install hooks ran `sudo`, `chsh`, `systemctl`, `git clone` and `curl`, and swallowed failures into a warning | the `user` shape, the service shape, archives (ADR 0176, 0177, 0205) | the agent module writes under `/etc` from its own tool through `sudo` (no keep-original, no give-back); the prompt's helper downloads itself unpinned; that the operator escalates without a prompt is assumed by three modules and declared by none (research 027) |
-| **Secrets in environment files** | `.env` files left world-readable; the decryption key beside what it decrypts; a deleted secret stayed in the file, so rotation was a no-op | the vault (ADR 0113, 0114) | a predecessor file of secrets is still sourced into the graphical session on two machines (research 027 Q2) |
-| **Symlinks into a home** | a system file linked into a person's home | ADR 0012 | on the control machine, a fail2ban action file is still a predecessor link into its home tree. Deleting that tree would silently break the repeat-offender jail. The mesh's fail2ban module must own it as a file first |
-| **Green while broken** | a recorded version frozen for four months; a verifier passing what it skipped | ADR 0134, 0145, 0184 | issue 230: a plan waiting for ever reads as healthy |
-
-## What it means here
-
-- **Research 026:** the desktop's 88 flavor-gated files and 92 theme variables are the flavor and
- templating failures in one module. Question 1 (one module per piece) and question 6 (hardware
- modules, settings later) are the answer, and nothing in the new modules may switch whole files on a
- setting.
-- **Research 027:** the hooks that installed the AUR helper, enabled the login manager and changed
- shells are what the `package`, `service` and `user` shapes replace. Every remaining `sudo` in a module's
- own code is a debt to be named, starting with the agent module.
diff --git a/01-RESEARCH/026-the-graphical-session-as-modules/04-screensaver-displays-and-menus.md b/01-RESEARCH/026-the-graphical-session-as-modules/04-screensaver-displays-and-menus.md
deleted file mode 100644
index 44530c91..00000000
--- a/01-RESEARCH/026-the-graphical-session-as-modules/04-screensaver-displays-and-menus.md
+++ /dev/null
@@ -1,129 +0,0 @@
-# 04 — Screensaver, displays and menus
-
-Three areas the operator named on 2026-10-04, as their own modules. Each sharpens a row of
-[01](01-what-the-workstations-run.md) and a question of [02](02-the-questions-and-the-options.md).
-
-## The screensaver: idle, lock and display power
-
-**Measured on both workstations:**
-
-- **Idle and lock** are three things wired by hand in the session's start script:
- - the X screensaver timeout (`xset s 1800`);
- - the display power timeouts (`xset dpms`);
- - `xss-lock` running the colour build of `i3lock` through a wrapper, in a respawn loop.
-- **A second screensaver,** xscreensaver, is installed and deliberately not started. Earlier it
- overrode the display power settings with its own, and locked nothing. Its configuration file is
- still in the home.
-- **The lock screen's 20-odd colours and formats** were predecessor theme variables.
-- **The colour build is not in the official repositories** (research 026/01).
-
-**Starting position:**
-
-- **One module for the lock screen,** holding `node-lock-screen`: the locker and its wrapper as the
- module's own files, the screensaver and display power timeouts, and `xss-lock`.
-- The timeouts and colours are its defaults, and settings later (issue 168).
-- The colour build ships as ADR 0205's pinned archive, or the module uses the official `i3lock`.
- That is the operator's choice, and the colours are the only difference.
-- xscreensaver is not a module; its package and file are removed.
-- `xss-lock` needs the logind session, so it stays a session-start line contributed into
- `.xinitrc`'s block (question 4), not a unit.
-
-## Monitor layout (xrandr)
-
-**Measured:**
-
-- Each workstation has a layout script generated by `arandr`, with the monitor names baked in. One
- workstation also has several layouts for named places, a hotplug rule and a wizard.
-- **The desktop carried the laptop's layout scripts.**
-- No `xorg.conf.d`, and no layout tool beyond the scripts.
-
-**Starting position: `autorandr`** (official repositories) inside the display server's module.
-
-- `autorandr` saves a layout as a profile **keyed by the connected monitors' identities** (their EDID)
- and applies the matching one at login and on hotplug.
-- Profiles therefore need no machine's name. A profile can be shared mesh-wide and simply never
- matches on a machine without those monitors. That is exactly the "say it by what is there, never by
- a name" rule (ADR 0112).
-- The profiles are the operator's data, saved by the tool itself, so they are *found* (ADR 0182). A
- `layout` verb on `node-display-server` lists, saves and applies them.
-- The arandr scripts and the hotplug rule retire once a profile exists for each.
-
-## Menus: rofi and dmenu
-
-**Measured:**
-
-- rofi is the launcher, the power menu, the theme picker and the clipboard menu.
-- The operator's scripts call `rofi -dmenu` in four places and **plain `dmenu` in two. dmenu is
- installed on neither workstation, so those two fail.**
-
-**Starting position:**
-
-- **`rofi` holds `node-launcher`**, and the seat's protocol includes a **dmenu-compatible command**:
- read choices on standard input, print the chosen one. Scripts call that command, not a program by
- name.
-- **`dmenu` is a module of its own** (official repositories), able to hold the same seat on a machine
- that wants it, for instance a Wayland session where `wofi` or `fuzzel` would hold it instead.
-- The rofi module carries its theme files, and the menus that belong to other modules arrive as those
- modules' scripts:
- - power menu → the session;
- - clipboard menu → the clipboard module;
- - theme picker → settings, once issue 168 closes.
-
-## The clipboard: xclip and greenclip
-
-**Measured:**
-
-- **greenclip** keeps the clipboard's history, and rofi shows it on a key binding.
-- **greenclip is not in the official repositories.**
-- It is started two ways: the window manager's configuration starts it on both workstations, and on
- one a user unit is enabled as well.
-- Its configuration names an absolute home path.
-- **xclip** (official) is the command-line clipboard the operator's scripts use.
-
-**Starting position:**
-
-- **`xclip` is a module of its own,** a package and nothing else. It is the tool scripts depend on,
- and a module that needs it requires it.
-- **The clipboard manager holds `node-clipboard`:** its daemon, started once by the session (a session
- contribution, or a user unit once user-scoped units ship, never both), its configuration with no
- absolute path, and its menu binding contributed to the window manager.
-- **Which manager holds it is the operator's choice:**
- - greenclip, as today, shipped under ADR 0205;
- - or `clipmenu` (official), which feeds the same dmenu-compatible command as the launcher seat above,
- and needs no archive.
-- **On Wayland** the same seat is held by `cliphist` with `wl-clipboard`, both official.
-
-## Fonts
-
-**Measured:**
-
-- The fonts the desktop uses are **hand-copied files** in the account's font directory, not packages:
- - a Nerd font for the window manager, the bar and the terminal;
- - a second one for the prompt;
- - on one workstation, the same four files twice, once under URL-encoded names;
- - on the other, a different build of the same font and three more copied from a theme's repository.
-- The system's default monospace is a different font (`Noto Sans Mono`), so anything that asks for
- `monospace` gets another face than the terminal.
-- The DPI is fixed in an X resource.
-- **Every Nerd font in use is in the official repositories** (Hack, Meslo, Iosevka, JetBrains Mono).
-
-**Decided** (the operator left the choice open, except that it must not be today's Hack):
-
-| role | face | why |
-|---|---|---|
-| monospace: terminal, window manager, bar, launcher, prompt | **JetBrains Mono Nerd Font** | built for long reading in a terminal, unambiguous `0O1lI`, optional ligatures; a version-3 Nerd font, so every icon the prompt and bar use is present |
-| interface: GTK, Qt, notifications | **Inter** | designed for screens, clear at small sizes |
-| icons missing from any face | Nerd Fonts Symbols | a fallback, so a font without icons still shows them |
-| emoji | Noto Color Emoji | |
-| serif and every other script | Noto | |
-
-All five are official packages.
-
-**Starting position:**
-
-- **A `fonts` module:** those packages, and a fontconfig file it owns that maps `monospace`, `sans-serif`,
- `serif` and the emoji and symbol fallbacks to the chosen faces, so every program agrees.
-- The terminal, bar, launcher and prompt modules name the family, not a file.
-- The DPI becomes the display server's setting (issue 168).
-- The copied files are removed by the operator once the packages are in (ADR 0182).
-- Fonts are not a seat: several coexist. The module owns the one place where *the* default is said.
diff --git a/01-RESEARCH/026-the-graphical-session-as-modules/05-the-tools-each-module-serves.md b/01-RESEARCH/026-the-graphical-session-as-modules/05-the-tools-each-module-serves.md
deleted file mode 100644
index efa669ed..00000000
--- a/01-RESEARCH/026-the-graphical-session-as-modules/05-the-tools-each-module-serves.md
+++ /dev/null
@@ -1,77 +0,0 @@
-# 05 — The tools each module serves
-
-A first catalogue for the modules of research 026 and 027, as the operator asked: "all kinds of useful
-tools for all these modules". Each tool is served by the node's runtime (ADR 0175), on the machine the
-module runs on. Through discovery (ADR 0195) it is reachable from any machine as
-`/.`, or as `/.` where a seat defines it.
-
-**Conventions:**
-
-- **(r)** reads.
-- **(a)** acts on the machine, escalating where it must, as the packet filter does (to-be 38 WP4).
-- **(d)** is a desktop act that needs the operator's session.
-- A tool that changes something a module declares says so in its answer: the next push restores the
- declaration.
-- Every tool answers structured data, not prose (issue 229).
-- **Seat verbs** (marked *seat*) are the protocol every holder of that seat serves. The rest are the
- module's own.
-
-## The graphical session (026)
-
-| module | tools |
-|---|---|
-| `xorg` (*node-display-server*) | *seat* `displays` (r: outputs, modes, rates, connected monitors with their identity) · *seat* `layout` (r/a: list, save, apply an autorandr profile) · `set-mode` (a: one output's resolution, rate, rotation, scale) · `primary` (a) · `dpi` (r/a) · `input-devices` (r) · `input-set` (a: touchpad tap, natural scroll, pointer speed) · `keyboard` (r/a: layout and options) · `screenshot` (d: one screen or all, as a file) · `x-log` (r: the server's errors since start) |
-| `i3` (*node-display-session*) | *seat* `reload` (a) · *seat* `workspaces` (r) · *seat* `windows` (r: tree with classes, titles, workspaces) · `focus` (d: window or workspace) · `move` (d: window to workspace or output) · `layout-save` / `layout-restore` (d: a workspace's arrangement) · `exec` (d: start a program in the session) · `kill` (d) · `bindings` (r: every key binding and what it runs) · `config-check` (r: validate the composed configuration before a reload) · `marks` (r) · `scratchpad` (d) |
-| `sway` (*node-display-server*, *node-display-session*) | the same seat verbs over Wayland, plus `outputs` (r) and `idle-inhibitors` (r) |
-| `lemurs` (*node-login-manager*) | *seat* `sessions` (r: what the login screen offers) · *seat* `default-session` (r/a) · `logins` (r: who logged in when, from the journal) |
-| `xterm` (*node-terminal-emulator*) | *seat* `open` (d: a terminal, optionally running a command, in a directory) · `font` (r/a: face and size) · `colours` (r) |
-| `i3status-rust` (*node-bar*) | *seat* `reload` (a) · `blocks` (r: what the bar shows and each block's current value) · `block-run` (r: run one block once and answer its output) · `themes` (r) |
-| `picom` (*node-compositor*) | *seat* `restart` (a) · `rules` (r: opacity, shadow and blur rules in force) · `window-opacity` (d) · `toggle` (d: compositing off and on, for a game or a test) |
-| `rofi` (*node-launcher*) | *seat* `menu` (d: show a list, answer the chosen line: the dmenu-compatible command as a tool) · `applications` (r: the desktop entries it would offer) · `themes` (r) · `run` (d) |
-| `dmenu` (*node-launcher*) | *seat* `menu` (d) |
-| `dunst` (*node-notifier*) | *seat* `send` (d: title, body, urgency, actions) · *seat* `history` (r) · `pause` / `resume` (d: do not disturb) · `close-all` (d) · `rules` (r) · `count` (r: shown, waiting, history) |
-| lock module (*node-lock-screen*) | *seat* `lock` (d) · `idle` (r/a: screensaver and display power timeouts) · `inhibit` (d: keep the screen on for a while) · `locked` (r: is the session locked now, and since when) |
-| clipboard manager (*node-clipboard*) | *seat* `history` (r: entries, newest first, length-limited) · *seat* `copy` (d: put text on the clipboard) · `paste` (r: what the clipboard holds now) · `clear` (d) · `delete` (d: one entry) |
-| `xclip` | `copy` (d) · `paste` (r): the plain clipboard without a manager |
-| `feh` (wallpaper) | `set` (d: an image, per output) · `current` (r) |
-| `fonts` | `families` (r: installed faces) · `match` (r: what `monospace`, `sans-serif` and `emoji` resolve to) · `glyph` (r: which installed font has a given character) · `cache-rebuild` (a) |
-| theme module | `appearance` (r/a: dark or light, for GTK, Qt and the portal at once) · `cursor` (r/a) · `icons` (r) · `portal-check` (r: which portal backend answers which interface) |
-| `gnome-keyring` (*node-secret-service*) | *seat* `unlocked` (r) · `lock` (d) · `collections` (r: names and item counts, never secrets) · `ssh-keys` (r: what the agent holds, by fingerprint) |
-| desktop hardware module (laptop) | `brightness` (r/a: panel and keyboard) · `battery` (r: charge, health, cycles, limit) · `charge-limit` (r/a) · `gpu-mode` (r/a: integrated, hybrid, discrete) · *seat* `profile` (r/a: quiet, balanced, performance) · `thermals` (r: temperatures and fan speeds) · `power-draw` (r) |
-
-## The system and the account (027)
-
-| module | tools |
-|---|---|
-| `docker` (*node-container-runtime*, ADR 0166) | *seat* `list`, `inspect`, `logs`, `stats`, `start`, `stop`, `restart` (r/a) · `images` (r: with size and which container uses each) · `prune` (a: dangling images, stopped containers not held by the mesh, build cache, with a dry run first) · `disk-usage` (r) · `networks` (r) · `volumes` (r: with what mounts each and whether the mesh holds it) · `events` (r: the last hour) · `daemon-config` (r) |
-| `docker-compose` | `projects` (r: compose projects running and where their files are) · `up` / `down` / `restart` (a: one project, by directory) · `logs` (r) · `ps` (r) |
-| `sudo` | `rules` (r: what the account may run, without a prompt and with one) · `check` (r: does the escalation the mesh relies on work here) |
-| `pacman` | `search` (r) · `installed` (r: with version and explicitly or as a dependency) · `info` (r) · `owns` (r: which package owns a path) · `files` (r) · `updates` (r: what an upgrade would change) · `upgrade` (a: with the news first) · `orphans` (r) · `remove-orphans` (a) · `cache` (r/a: size, clean to the last N versions) · `history` (r: installs and upgrades from the log) · `mirrors` (r/a: rank and refresh) · `news` (r: distribution news since the last upgrade) |
-| AUR (package repository, 027 question 1) | `search` (r) · `build` (a: on the build machine, into the mesh's repository) · `outdated` (r) · `published` (r) |
-| `snapd`, `flatpak` | `list` (r) · `install` / `remove` (a) · `update` (a) · `runtimes` (r) · `disk-usage` (r) |
-| `time-sync` | `status` (r: synchronised, offset, server) · `servers` (r) · `sync-now` (a) |
-| `localization` | `get` (r: locale, time zone, keymap) · `time-zone` (r/a) · `locales` (r) |
-| `kernel` | `running` (r: version, command line, uptime) · `installed` (r) · `modules` (r: loaded, with what uses them) · `reboot-needed` (r: a newer kernel or library than the one running) · `microcode` (r) · `boot-entries` (r) · `initramfs-rebuild` (a) · `dmesg` (r: errors since boot) |
-| `logrotate` | `status` (r: last rotation per log) · `force` (a: one configuration) · `big-logs` (r: the largest logs on the machine) |
-| `avahi` | `browse` (r: services on the local network) · `resolve` (r) |
-| `cups` | `printers` (r) · `queue` (r) · `cancel` (a) · `print` (a: a file to a printer) · `default` (r/a) |
-| `bluetooth` | `devices` (r: paired, connected, battery where reported) · `connect` / `disconnect` (a) · `scan` (r) · `power` (r/a) |
-| `ssh-client` (owns `~/.ssh`) | `hosts` (r: every `Host` and where it came from: the mesh, a module, the operator) · `check` (r: modes, keys without a passphrase, keys unused for a year, stale `known_hosts` entries) · `authorized` (r: who may log in, by fingerprint and comment) · `revoke` (a: one authorized key, into the operator's region) · `known-host` (r/a: verify, refresh one host's key) · `test` (r: can this machine reach a host and authenticate, batch mode) |
-| `sshd` | `sessions` (r: who is logged in, from where) · `config-effective` (r: `sshd -T`) · `failed-logins` (r: since a time, with fail2ban's verdicts) |
-| scripts modules | `list` (r: each script with its one-line description) · `run` (a: one script by name with arguments, as the account, bounded like `execute`) · `which` (r: which module ships a command) |
-| `node-env` (*node-environment*) | `show` (r: every variable and `PATH` entry with the module that contributed it) · `diff` (r: what a shell actually has versus what the mesh composed) |
-| `zsh` (*node-login-shell*) | *seat* `execute` · `zsh_config` (r) · `history-search` (r: the account's history, by pattern) · `functions` (r: aliases and functions in force, with where each came from) · `startup-time` (r: how long an interactive shell takes to start, per slot) |
-| `memory-pressure` | `status` (r: memory, swap, compressed swap ratio, pressure stall) · `top` (r: the largest processes) · `oom-history` (r: what was killed, when) |
-| `zfs` | `pools` (r: health, capacity, fragmentation) · `datasets` (r) · `snapshots` (r/a: list, create, destroy by name) · `scrub` (r/a: status, start) · `errors` (r) · `arc` (r: cache statistics) |
-| `nfs-server`, `samba` | `exports` / `shares` (r) · `clients` (r: who has it mounted now) · `reload` (a) |
-| `nfs-client`, `smb-client` | `mounts` (r: each share, mounted or not, and since when) · `mount` / `unmount` (a) · `test` (r: is the server reachable, is the export offered) |
-| hosts-file holder (*node-hosts-file*, ADR 0199) | *seat* `entries`, `add`, `remove` |
-| `vnstat`, `lm_sensors` | `traffic` (r: per interface, day, month) · `sensors` (r) |
-| mail consumer (future effort) | `accounts` (r) · `search` (r) · `unread` (r) · `read` (r: one message) · `mark` (a) · `send` (a) |
-
-## What this catalogue is for
-
-It is a starting list, not a contract. A tool becomes a contract only when it is a seat's verb, and
-each seat's verbs are decided in that seat's record (ADR 0132). A module's own tools can grow freely.
-Every row above is a tool the operator would otherwise run by hand over ssh. That is the measure of
-whether one is worth writing.
diff --git a/01-RESEARCH/027-the-system-layer-as-modules/00-overview.md b/01-RESEARCH/027-the-system-layer-as-modules/00-overview.md
deleted file mode 100644
index a95b1d40..00000000
--- a/01-RESEARCH/027-the-system-layer-as-modules/00-overview.md
+++ /dev/null
@@ -1,64 +0,0 @@
----
-status: active
-initiated: 2026-10-04
-touches:
- - 02-DECISIONS/0165-container-runtime-is-what-a-machine-can-run-and-a-running-runtime-is-its-holders-health.md
- - 02-DECISIONS/0166-the-container-runtime-is-a-node-seat-and-the-host-creates-containers-through-its-holder.md
- - 02-DECISIONS/0203-the-accounts-environment-is-one-modules-and-every-module-contributes-to-it.md
- - 02-DECISIONS/0205-software-the-distribution-does-not-package-ships-as-a-pinned-archive-of-the-module.md
- - 03-DESIGN/01-to-be/37-the-operators-machine.md
- - 03-DESIGN/01-to-be/38-building-the-operators-machine.md
-became: []
----
-
-# 027 — The system layer as modules
-
-## What is investigated
-
-What runs on the machines below the operator's home and outside the mesh's own services, and which
-of it should be modules. That covers:
-
-- the container runtime and its tools;
-- privilege (sudo);
-- the package manager and the software it cannot install;
-- time, locale, the kernel and boot;
-- log rotation;
-- the machine-specific daemons the workstations and servers carry: printing, bluetooth, VPN
- clients, virtualisation, storage, sharing.
-
-## Why
-
-The operator asked for the system level beside the graphical session. In particular:
-
-- a `docker` module (decided in principle by the proposed ADRs 0165 and 0166, never built);
-- a `docker-compose` module for development work, assigned **only to the two workstations**.
-
-Measured in [01](01-what-the-machines-run.md): on four machines, almost nothing at this level is
-owned by a module. The pieces differ by machine for no recorded reason. Three findings are security
-matters on their own.
-
-## How it is approached
-
-**Adopting is also improving** (the operator, 2026-10-04). A module is not a copy of what a machine
-does today. Making it is the moment to fix what is broken, drop what is dead, choose the better tool
-and remove the leftovers. Every module's design lists its improvements over today. **Every module
-also serves tools,** many of them, for reading, acting and diagnosing; a module that only places a
-package and a file is unfinished. The tools are catalogued in
-[026/05](../026-the-graphical-session-as-modules/05-the-tools-each-module-serves.md).
-
-## What it touches
-
-- **The container runtime seat** (ADRs 0165 and 0166, both proposed).
-- **The host's `package` shape**, which installs from the distribution's official repositories only,
- while the workstations carry 67 and 114 packages from elsewhere.
-- **How a secret reaches the account's environment.** ADR 0203 forbids it in the contributed
- environment, but a predecessor file supplies such secrets today.
-- **The facts the mesh assumes and never declares,** above all that the operator account escalates
- without a prompt.
-
-## Documents
-
-- [01 — What the machines run](01-what-the-machines-run.md): evidence.
-- [02 — Candidates and questions](02-candidates-and-questions.md)
-- [03 — The account's own tools](03-the-accounts-own-tools.md): `~/.ssh` as one module's, scripts on every machine, the keyring, the laptop's power management, mail as events
-- The predecessor's lessons, shared with research 026: [026/03](../026-the-graphical-session-as-modules/03-what-the-predecessor-taught.md)
diff --git a/01-RESEARCH/027-the-system-layer-as-modules/01-what-the-machines-run.md b/01-RESEARCH/027-the-system-layer-as-modules/01-what-the-machines-run.md
deleted file mode 100644
index 0810cecc..00000000
--- a/01-RESEARCH/027-the-system-layer-as-modules/01-what-the-machines-run.md
+++ /dev/null
@@ -1,103 +0,0 @@
-# 01 — What the machines run
-
-Measured 2026-10-04 on four machines, read-only, including the host's own record of what it applied:
-two servers (the anchor and a home server) and two workstations (a laptop and a desktop). "Owned"
-means a module the mesh assigns declares it.
-
-## The container runtime
-
-| | anchor | home server | laptop | desktop |
-|---|---|---|---|---|
-| docker | 29.8.2 | 29.8.2 | 29.7.2 | 29.7.2 |
-| compose | 5.5.1 | 5.6.0 | 5.5.0 | 5.5.0 |
-| buildx | 0.37.2 | — | — | — |
-| podman | — | 6.1.3 | 6.1.0 | 6.1.0 |
-| `docker.socket` | disabled | enabled | enabled | enabled |
-| `containerd.service` | disabled | disabled | disabled | **enabled** |
-| `daemon.json` beyond the shared keys | direct routing, two more insecure registries | log rotation (100 MB × 10) | — | — |
-| docker group | operator, **a CI user** | operator | operator | operator |
-
-**Ownership:**
-
-- The `docker` package is owned on one machine only, by the installer's bootstrap, not by a module.
-- `docker.service` is declared indirectly, by the name resolver and the private-network modules,
- which each merge their own keys into `daemon.json`.
-- Nothing owns the socket, containerd, compose, buildx or the group.
-
-**Compose in use:**
-
-- On the servers, no running container belongs to a compose project. Their compose files are
- pre-mesh trees under the operator's and root's homes, plus a dangling enabled unit for one of them.
-- On the workstations, compose runs development stacks, and pre-mesh service trees sit under a
- top-level directory.
-
-The mesh marks its own containers with a host label. On the workstations, a handful of unlabelled
-development and test containers run beside its build agent.
-
-## Privilege
-
-- The operator account escalates **without a prompt on all four machines**. The mesh relies on this,
- but it is set by hand in `/etc/sudoers` (a `wheel` rule on two machines, the account named on
- two), and nothing declares it.
-- On the anchor, a **CI user from the predecessor** keeps passwordless sudo and docker membership,
- and a predecessor drop-in in `sudoers.d` survives.
-- On the desktop, the operator account is also in the **`root` group**.
-
-## The package manager
-
-- `pacman.conf` is stock except on one server (parallel downloads).
-- The mirror list was generated once by a tool that is no longer installed. On the anchor, it is the
- hosting provider's single mirror.
-- An AUR helper is installed everywhere.
-- **Packages from outside the official repositories:** 2 on the anchor, 21 on the home server,
- 67 on the laptop, 114 on the desktop. They include:
- - the agent CLI, which a catalogue module declares as a package and the host cannot install;
- - a VPN client;
- - a remote-access client;
- - printer drivers;
- - GPU tools;
- - a kernel module built from source (DKMS) for a storage filesystem;
- - a snap daemon.
-
-## Time, locale, kernel, boot
-
-| | anchor | home server | laptop | desktop |
-|---|---|---|---|---|
-| time zone, keymap | **another zone**, a non-US console keymap | local zone, unset | local zone, unset | local zone, unset |
-| time sync | timesyncd plus a provider drop-in | timesyncd | timesyncd | **ntpd**, timesyncd disabled |
-| bootloader | grub (BIOS) | systemd-boot **and** grub | systemd-boot | systemd-boot **and** grub |
-| kernels | one | two, plus a DKMS filesystem module | one | one, plus a DKMS controller driver |
-| microcode | **none** | yes | yes | **none** |
-| swap | RAID partition | partition | zram, a file and a partition | partition |
-| log rotation timer | not found | enabled | not found | not found |
-
-## Daemons and services no module owns
-
-- **All four:** avahi.
-- **Workstations:**
- - a VPN client daemon (both);
- - virtualisation (incus) with a hand-made unit that inserts container-runtime firewall rules (both);
- - printing and bluetooth;
- - GPU and power tuning per model;
- - a remote-access daemon (laptop);
- - snap and flatpak (desktop);
- - the local model server, run from a hand-written unit although a catalogue module for it exists
- (desktop);
- - Samba sharing and a network filesystem mount from the home server (desktop). A second mount is
- failing, and its **credential is written in clear in `/etc/fstab`**.
-- **Servers:**
- - a storage pool (about 167 TB) with its import, mount and scrub units, an NFS server and Samba
- sharing (home server);
- - traffic and sensor monitoring (home server);
- - a DHCP client daemon the catalogue has a module for but does not assign there (home server);
- - cron, an entropy daemon, and the **legacy `iptables` services**, which run beside the mesh's own
- filter (anchor).
-- **Not found anywhere:** a backup agent, a monitoring agent, a second VPN mesh.
-
-## What is plain debris
-
-- Dangling enabled-unit links on three machines.
-- Predecessor blocks in `/etc/hosts` on both servers.
-- The CI user, and the predecessor sudoers drop-in, on the anchor.
-- Pre-mesh compose trees on the anchor, the home server and the desktop.
-- Unlabelled test containers on the workstations.
diff --git a/01-RESEARCH/027-the-system-layer-as-modules/02-candidates-and-questions.md b/01-RESEARCH/027-the-system-layer-as-modules/02-candidates-and-questions.md
deleted file mode 100644
index b6be40b2..00000000
--- a/01-RESEARCH/027-the-system-layer-as-modules/02-candidates-and-questions.md
+++ /dev/null
@@ -1,128 +0,0 @@
-# 02 — Candidates and questions
-
-## Decided by the operator on 2026-10-04
-
-- **`docker`** holds the container runtime seat on every machine, as ADRs 0165 and 0166 propose. Those
- records are promoted from proposed when it is built.
-- **`docker-compose` is a module of its own,** the distribution's package and nothing else. It is
- assigned **only to the two workstations**, for development work. The servers run nothing through
- compose.
-
-Later the same day, on the candidates below:
-
-- **Yes, all of them:** `docker`, `docker-compose`, `sudo`, `pacman`, an AUR helper (question 1),
- `time-sync`, `kernel` (with boot and microcode), `logrotate`, `avahi`, `cups` with the printer's
- driver, and every server-only candidate.
-- **Locale, time zone and keymap are one module, `localization`.**
-- **`snapd` and `flatpak`** are modules, on the two workstations only.
-- **`incus` is the lab's,** whose module depends on it. It is not a module of its own beside the lab.
-- **The agent's and the local model server's modules are still being developed,** and are not
- assigned until they are.
-- **The predecessor's CI user is retired.** It was removed from the anchor the same day, with its
- sudoers line, its docker membership and a dangling unit link; the backup is on the machine.
-
-## Candidate modules
-
-**On every machine:**
-
-| module | owns | first reason |
-|---|---|---|
-| `docker` | the packages (runtime, containerd), the service and socket, `daemon.json`'s base keys (live restore, log rotation), the docker group's members | four machines, four configurations, one owner on one |
-| `sudo` | the operator account's escalation as a drop-in, declared | the mesh's tools rely on it (to-be 38 WP4) and nothing states it |
-| `pacman` | `pacman.conf`'s few keys, the mirror list and its refresher, cache cleaning | mirrors generated once and never again |
-| `time-sync` | timesyncd and its drop-ins | two daemons across four machines |
-| `localization` | locale, time zone, console keymap (one module, the operator's choice) | one machine differs, with no record why |
-| `kernel` | the kernel packages, microcode, initramfs presets | two machines without microcode |
-| `logrotate` | the timer and the base configuration | rotation runs on one machine of four |
-| `avahi` | the daemon and name-service switch entry | on all four, owned by none |
-
-**On the workstations only:**
-
-- `docker-compose`;
-- `lemurs`, the login manager (research 026);
-- a VPN client module;
-- `incus` with its forward unit (the lab module declares the package on one workstation only);
-- `cups` with the printer's driver;
-- `bluetooth`;
-- per-model **hardware** modules: GPU, power, vendor keys, brightness. These are the same modules
- research 026 needs for the desktop's fragments.
-
-**On the servers only:**
-
-- `zfs` with its scrub timer, and the long-term kernel it builds against;
-- `nfs-server`;
-- `samba`;
-- `vnstat`, `lm_sensors`.
-
-`cron` on the anchor serves one stock file and can go. So can the entropy daemon on a modern
-kernel.
-
-**Retire, not model:** the legacy `iptables` services on the anchor. They duplicate the mesh's
-filter, which is ADR 0100's ground.
-
-## Questions this effort has to answer
-
-1. **Software outside the official repositories.** The host's `package` shape installs from the
- official repositories only. A catalogue module already declares an AUR package (the agent CLI),
- which no machine could install, and the workstations carry 181 such packages between them.
-
- | | option | for | against |
- |---|---|---|---|
- | P1 | ADR 0205's pinned vendored archive, per piece | exists | wrong for packages that build native code or kernel modules |
- | P2 | **The build machine builds AUR packages into a package repository the mesh serves** from its artifact store. The host then installs them as packages, signed | one shape for every package; pinned, reviewed and built once | a repository to serve and a signing key to keep |
- | P3 | An AUR helper on each machine, driven by the host | nothing to serve | builds on every machine, unpinned: the predecessor's `git clone` in another form |
-
- Starting position: P2, for anything with native code. ADR 0205 stays for plain files such as a
- theme.
-
-2. **Secrets in the account's environment.** A predecessor file feeds package-registry and API tokens
- to the session. ADR 0203 refuses secrets in contributed values, because they travel in the clear.
- The candidate is ADR 0182's third class: a module's own process writes a mode-0600 file of
- exports, from secrets the vault hands it over the bus, and the shell and the session source it.
- This needs its own record.
-
-3. **Per-machine sizing and drivers.** The swap layout, the GPU, the storage pool and the boot
- loader are facts of one machine's hardware. They belong in hardware modules, or in settings
- (issue 168), not in the shared ones.
-
-4. **What a module may leave behind.** Compose is installed on both servers, unowned. The mesh
- removes nothing it did not make. The choice is between an operator's one-off removal and a
- server-side `absent` declaration.
-
-5. **The hosts file.** ADR 0199 (decided on an open change, not yet merged)
- gives `/etc/hosts` to one module through a seat, `node-hosts-file`, with an operator region and
- three verbs. It is not built. Today the private network's foundation writes only its own block, and
- the rest of each file is a predecessor's stale blocks (both servers) or the operator's development
- names (both workstations). The candidate module is that seat's first holder. It takes the
- private-network block as a contribution, and its operator region replaces the hand-kept lines.
-
-6. **Mounts.** The host has no shape for a filesystem mount; ADR 0091 is about what a container
- mounts. One workstation mounts a share of the home server over NFS, and a second share over SMB.
- That second one fails, and its credential sits in clear in `/etc/fstab`.
-
- | | option | for | against |
- |---|---|---|---|
- | M1 | **A module owns `/etc/fstab`** and other modules contribute lines | one file, as people know it | the file also carries the root and boot filesystems the installer wrote, which no module should rewrite; a slot contribution into a file that can stop a machine booting |
- | M2 | **Each client module writes its own systemd mount (and automount) unit**, which is the service manager's drop-in for exactly this. The `nfs-client` or `smb-client` module declares the unit file and the service shape enables it. `/etc/fstab` stays the machine's | no new host shape, and no shared file; the unit names its own dependencies (network online, the private network) and an automount does not hang a boot when the server is away; unassigning removes the mount | a mount reads as a unit, not a line |
- | M3 | A new `mount` shape in the host | the host knows what a mount is | a second way to say what M2 says |
-
- Starting position: **M2.** The credential an SMB mount needs is a secret, written by the module's
- own process from the vault, mode 0600, which is question 2's mechanism. The pair is a server
- module exporting (`nfs-server`, `samba`) and a client module mounting. The client requires the
- share the server provides, so the mount is resolved, not hand-typed.
-
-7. **Two DHCP clients on one interface.** The home server runs `dhcpcd`, a DHCP *client* (no machine
- runs a DHCP server), next to the network manager, which is its assigned networking module. Both
- lease an address on the same interface, which therefore carries two LAN addresses. The catalogue's
- `dhcpcd` module is assigned nowhere, and this unit is a leftover. The network manager is the
- machine's one DHCP client, and `dhcpcd` should be disabled there.
-
-## Security findings, independent of any module
-
-1. A filesystem credential in clear text in a workstation's `/etc/fstab`, for a mount that is failing
- anyway.
-2. A predecessor CI user with passwordless sudo and docker membership on the anchor, and a
- predecessor sudoers drop-in. *The user was removed on 2026-10-04; the drop-in remains.*
-3. The operator account in the `root` group on one workstation.
-
-Each is one small change. None waits for a module.
diff --git a/01-RESEARCH/027-the-system-layer-as-modules/03-the-accounts-own-tools.md b/01-RESEARCH/027-the-system-layer-as-modules/03-the-accounts-own-tools.md
deleted file mode 100644
index 00c596bc..00000000
--- a/01-RESEARCH/027-the-system-layer-as-modules/03-the-accounts-own-tools.md
+++ /dev/null
@@ -1,169 +0,0 @@
-# 03 — The account's own tools: ssh, scripts, mail
-
-Three further directions from the operator on 2026-10-04. Each is account-level, like the shell
-([to-be 41](../../03-DESIGN/01-to-be/41-the-shell-and-the-accounts-environment.md)).
-
-## `~/.ssh` is one module's
-
-*"A module owns `~/.ssh`, so it is its responsibility that every folder is set up consistently and
-correctly."*
-
-**Measured:**
-
-- The catalogue's `ssh-client` module owns the directory (mode 0700) and one region of
- `~/.ssh/config`: a `Host` block per machine of the mesh. It owns nothing else.
-- On one workstation, a predecessor's header, `Include` and hand-written host block sat **above** the
- mesh's region. ssh takes the first match, so the predecessor's entries were the ones in force, for
- the same machines. Removed on 2026-10-04.
-- On the control machine, two keys of a retired CI system were still in the operator's
- `authorized_keys`, able to log in as the operator. Removed the same day.
-- Permissions differ by file and by machine. Backups of the configuration lie beside it.
-
-**Starting position:** `ssh-client` becomes the holder of everything under `~/.ssh`, classified as
-ADR 0182 asks:
-
-| path | class | how |
-|---|---|---|
-| `~/.ssh/`, its mode, every file's mode | owned | the directory resource, plus a check verb that reports a file with the wrong mode |
-| `~/.ssh/config` | written into, the mesh's block **at the start** | the mesh's hosts win; the operator's lines after it are kept; an `Include config.d/*` line in the block |
-| `~/.ssh/config.d/` | owned by the contributing module | ssh's own drop-in: a work module adds its forge's host there (research 026 C1) |
-| `~/.ssh/authorized_keys` | written into, the mesh's block | the operator's keys as the mesh records them, and nothing a retired system left. The operator's own lines are kept below the block |
-| `~/.ssh/known_hosts` | written into, the mesh's block | every mesh machine's host key, so the first connection never asks |
-| private keys | found | never read and never written by the mesh; a key the mesh should hand out comes from the vault, through the module's own process (ADR 0182, third class) |
-
-The sshd module is the other half: the machine's side. It is already in the catalogue.
-
-## Scripts on every machine, shared and machine-specific
-
-*"All nodes should get some custom scripts, both node-specific and mesh-specific (shared)."*
-
-**Measured:** the operator's script folder holds 64 entries plus 33 in its `bin/`. It is under no
-version control, and exists only where it was copied. It mixes three kinds:
-
-1. scripts belonging to a module (the desktop's watchers, lock, menus; a laptop model's brightness);
-2. the operator's own tools;
-3. installers that modules have replaced.
-
-**Starting position:**
-
-- **The operator's scripts live in a repository of their own,** registered as any application is
- ([ADR 0015](../../02-DECISIONS/0015-applications-live-in-their-own-repository.md)), built as archives,
- unpacked into a directory the module owns under the home. `bin/` goes on `PATH` through an
- environment contribution ([ADR 0203](../../02-DECISIONS/0203-the-accounts-environment-is-one-modules-and-every-module-contributes-to-it.md)),
- and small functions go into the shell through a `shell` contribution
- ([ADR 0204](../../02-DECISIONS/0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md)).
-- **"Machine-specific" is said by assignment, never by naming a machine**
- ([ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md)). One repository
- holds several modules:
- - `scripts` (shared, on every machine);
- - `scripts-workstation`;
- - `scripts-media`;
- - and so on, each assigned where it applies.
-
- A script that belongs to a piece of software or hardware moves into that module instead. A flavor
- inside one module is what [research 026/03](../026-the-graphical-session-as-modules/03-what-the-predecessor-taught.md)
- says not to repeat.
-- **A script can also be a tool.** A script with a one-line description is served by the node's
- runtime, so it can be called through the mesh on any machine that has it.
-- A script that needs a secret gets it through question 2's mechanism, never from a file of
- environment secrets.
-
-## The keyring
-
-*"A keyring is also a good thing to create a module for."*
-
-**Measured on the two workstations, which both run GNOME Keyring:**
-
-- **On one, the keyring unlocks at login.** The login manager's PAM service includes `login`, which
- carries `pam_gnome_keyring`.
-- **On the other, it does not.** The PAM line is only in the screensaver's service, so at session
- start the window manager runs a script that asks for the password a second time and unlocks the
- keyring with it.
-- **On both, the session's start script starts the daemon again** with the ssh and gpg components,
- and exports the ssh agent's socket. The keyring's current release serves the ssh agent through a
- separate per-user socket unit instead.
-
-**Starting position:** a `gnome-keyring` module that holds a node seat, `node-secret-service` (the
-holder of the desktop's secret service; a password manager could hold it instead). It declares:
-
-- the package;
-- its lines in the login manager's PAM file, written into, never over (ADR 0102), so login unlocks it
- on every machine;
-- the ssh agent's user socket, once user-scoped units ship;
-- the agent's socket path as an environment contribution, which needs a machine fact for the
- account's runtime directory. ADR 0203 forbids `$` in values, so `$XDG_RUNTIME_DIR` cannot be
- written in one.
-
-The second unlock prompt and the second daemon start go away.
-
-## Mail as events
-
-*"Ideally a mail consumer with all my mail accounts configured, so my mail is recorded in the bus."*
-
-**Measured:**
-
-- The predecessor polled one work mailbox every minute. It **read an access token out of the mail
- client's process memory**, called a mail API with it, and raised a desktop notification per unread
- message. It worked only while the mail client ran, and stopped silently when the predecessor's units
- were retired.
-- Two further predecessor modules served mail tools, for one provider and for IMAP.
-- The mesh runs a mail server of its own for its domains.
-
-**Not decided here; it needs an effort of its own.** The questions it would have to answer:
-
-- **Accounts and how each authenticates:**
- - IMAP with an app password;
- - a provider's OAuth with a registered application;
- - the mesh's own mail server, which can publish delivery itself.
-
- An employer's tenant may forbid registering an application at all.
-- **What the bus records:**
- - headers and a summary as events;
- - bodies and attachments in an object store the event points at;
- - retention, since mail is the most personal data the mesh would hold.
-- **What consumes it:** a notifier bridge to the desktop (the predecessor's notifications), search,
- an agent's context.
-- **Where it runs:** one long-running module, not per machine (ADR 0198).
-
-The obvious first step is the mail server the mesh already runs.
-
-## Power management on the laptop
-
-*"Power management for the laptop."*
-
-**Measured on the laptop** (a gaming model with a hybrid GPU):
-
-- **The platform profile is driven by a vendor daemon** (`asusd`) and its CLI. The vendor CLI is
- now in the official repositories; the copy installed came from elsewhere. A predecessor script
- runs as a user unit and switches the profile every five seconds: quiet on battery, balanced on
- mains, performance above 50 % CPU.
-- **The hybrid GPU's mode** (now hybrid) is held by a second vendor daemon (`supergfxd`), which is
- **not** in the official repositories. Kernel-module options for the discrete GPU's power state
- and its suspend, hibernate and resume units are set by hand. Its own power daemon is masked.
-- **The battery charge limit is 80 %,** set by the vendor daemon.
-- **The lid and power key suspend.** The brightness key is ignored by logind and handled by the
- vendor-key path. Both are logind drop-ins.
-- **Memory pressure:** compressed swap in RAM (`zram`) beside a swap file and a partition;
- `systemd-oomd` with drop-ins; a predecessor *memory guard* user unit that notifies before the OOM
- killer acts.
-- `upower` runs. There is no `power-profiles-daemon`, `tlp`, `auto-cpufreq` or `thermald`, so nothing
- competes with the vendor daemon, by design.
-
-All of it came from two predecessor modules, one of which was a laptop-model *flavor*. A desktop
-received part of it (research 026/01).
-
-**Starting position:**
-
-- **A hardware module per machine model** (here, the laptop's model). It holds the vendor daemon and
- its profile configuration, the GPU mode daemon (ADR 0205's case, or the build machine's package
- repository of research 027 question 1), the discrete GPU's module options and suspend units, the
- logind drop-ins, the battery charge limit, and the vendor keys and brightness. It is assigned to
- the one machine of that model, and to any second one later.
-- **The profile switching** moves from a polling script to the module's own long-running code
- (ADR 0198). It reacts to the power-supply change event instead of polling, and its thresholds
- become settings (issue 168).
-- **Memory pressure is not the laptop's alone.** `zram` and `systemd-oomd` with the notifier are a
- `memory-pressure` module, assigned wherever wanted. The swap layout stays the machine's (`kernel`
- module, question 3).
-- A **`node-power-profile`** seat (vendor daemon, or `power-profiles-daemon` on other hardware)
- gives the mesh one verb, `profile`, the same on every machine that has one.
diff --git a/01-RESEARCH/028-the-meshs-output-channel/00-overview.md b/01-RESEARCH/028-the-meshs-output-channel/00-overview.md
deleted file mode 100644
index 20d7a90e..00000000
--- a/01-RESEARCH/028-the-meshs-output-channel/00-overview.md
+++ /dev/null
@@ -1,124 +0,0 @@
----
-status: graduated
-initiated: 2026-10-04
-touches:
- - 04-ISSUES/187-the-mesh-tells-nobody-when-it-stops-working/00-report.md
- - 04-ISSUES/229-a-rollout-cannot-be-followed-through-the-meshs-tools/00-report.md
- - 04-ISSUES/230-a-host-that-hands-over-to-a-newer-one-loses-its-report-and-a-plan-waits-for-ever/00-report.md
- - 04-ISSUES/233-a-host-without-its-package-managers-configuration-refuses-the-declaration-that-would-restore-it/00-report.md
- - 02-DECISIONS/0126-a-module-declares-its-own-seats.md
- - 02-DECISIONS/0208-the-graphical-session-is-one-module-per-piece-on-the-meshs-seats.md
- - 02-DECISIONS/0210-a-tools-configuration-is-its-seat-holders-and-every-other-module-extends-it-through-the-seat.md
- - 03-DESIGN/01-to-be/32-what-a-module-declares.md
- - 03-DESIGN/01-to-be/45-a-core-that-cannot-fail-silently.md
- - 02-DECISIONS/0190-a-seats-work-is-shared-by-its-holders-and-building-is-the-first-such-role.md
- - 02-DECISIONS/0212-a-seat-says-what-it-receives-and-the-machines-hotkeys-are-a-seat.md
- - 02-DECISIONS/0223-the-mesh-has-two-resolvers-and-a-machine-lists-only-them.md
- - 02-DECISIONS/0227-the-core-holds-nine-rules-each-checked-and-is-built-to-them-in-six-phases.md
- - 02-DECISIONS/0228-a-value-given-by-hand-lives-only-until-its-modules-first-good-start.md
- - 02-DECISIONS/0230-a-consumer-the-mesh-stops-asking-for-is-retired-and-deleted-only-by-a-person.md
- - 02-DECISIONS/0232-a-binding-to-a-consumers-data-moves-only-by-a-person.md
-became:
- - 02-DECISIONS/0234-the-mesh-holds-a-conversation-with-its-operator.md
- - 03-DESIGN/01-to-be/46-the-conversation-with-the-operator.md
----
-
-# 028 — The mesh's output channel
-
-## What
-
-How the mesh tells its operator what it noticed. The operator's framing: sending notifications
-is **an output channel for the mesh**. The mesh already knows when a machine stops answering, when
-a failure repeats and will not fix itself, when a rollout waits for ever. Today it keeps that to
-itself until someone asks.
-
-The effort looks at:
-
-- **the seat:** one, held once for the mesh, that every other part uses to say something to the
- operator;
-- **the channels**, each a module: a desktop notification on the machine the operator is at,
- **Telegram**, a phone push service, chat, mail and others (see [02](02-the-channels.md));
-- **the routing**, by severity and by where the operator is;
-- **the life of a message:** deduplicated while it holds, resolved when it stops, acknowledged or
- silenced by the operator;
-- **the watcher's watcher:** who tells the operator when the parts that would tell them are the
- ones that failed;
-- **the conversation** (widened 2026-10-06): the mesh, its modules and its agents send messages and
- **asks** (a question, a choice, a value, an approval), and the operator answers or writes first, over
- channels chosen by their declared **capabilities** and by the operator's **work context**. Inputs
- from outside (a mail arriving) share the same envelope;
-- **asks that authorise:** one layer on top, for the answers that perform an action. These are checked
- by the controller, and allowed only on channels whose capabilities prove that the operator answered.
-
-### Why this effort widened rather than a new one opened
-
-On 2026-10-06 the operator asked for these:
-- approving and rejecting through Telegram;
-- a generic shape in which Telegram simply holds a seat;
-- input triggers alongside output channels;
-- capabilities that decide which actions may travel on which channel;
-- the work context as a factor in choosing the channel;
-- asks that are not about permission at all.
-
-That could have opened a new effort. It did not, because:
-
-- every part of it hangs on this effort's open questions: Q1 (where a channel attaches), Q4
- (presence), Q7 (answering back) and Q8 (what may leave);
-- ADR 0227 kept answering back open **here**, and said this effort's graduation amends to-be 45 §5;
-- an answer belongs to the message or ask this seat sends. Splitting them would leave two efforts each
- owning half of one conversation.
-
-Input that is not an answer (a mail arriving, a webhook) shares the envelope and the seat shape, and
-is designed here only as far as that shape. Its consumers are later work.
-
-## Why
-
-[Issue 187](../../04-ISSUES/187-the-mesh-tells-nobody-when-it-stops-working/00-report.md) is the
-class: *the mesh tells nobody when it stops working*. [01](01-what-the-mesh-already-knows.md)
-counts it.
-- 15 of the 236 issue reports say the fault was found because a person happened to look.
-- 74 describe something failing silently.
-
-On the day this effort opened, the mesh knew three things and told nobody:
-- a workstation had refused every declaration for ninety minutes;
-- the same workstation had been out of touch for ten minutes after an upgrade;
-- one failure on the laptop had repeated thirteen times.
-
-Every one of them was in `status`, for whoever asked.
-
-The pieces exist. [To-be 32](../../03-DESIGN/01-to-be/32-what-a-module-declares.md) already uses a
-`telegram-sender` seat as its worked example of a work queue with retention. ADR 0208 made a
-machine's desktop notifier a node seat with a `send` verb. What is missing is a seat that speaks
-for the mesh, sources that call it, and channels that deliver.
-
-## What it touches
-
-- **The controller**, which would become the first source of what it already computes for `status`.
-- **The node-notifier seat**, which would become one channel among several.
-- **Issues 187, 229, 230 and 233**, each of which ends in "and nothing said so".
-- **ADR 0210**, because a channel extends the output seat through a contribution, and therefore
- depends on it.
-
-## Documents
-
-1. [What the mesh already knows](01-what-the-mesh-already-knows.md): the evidence, and the events
- that exist.
-2. [The channels](02-the-channels.md): the candidates, Telegram first, weighed on the same questions.
-3. [Open questions](03-open-questions.md): the seat, routing, life of a message, the watcher's
- watcher, what may leave the mesh.
-4. [Telegram, as the first holder](04-telegram-as-the-first-holder.md): making the bot, the bot
- API's limits and semantics, what Telegram sees, and ten defects in the built code.
-5. [The other holders, on the same axes](05-the-other-holders-on-the-same-axes.md): ntfy, Matrix,
- Pushover, Gotify, mail, Signal, SMS and a dead-man service, as away channel and as the watcher's
- path.
-6. [A conversation with the operator](06-a-conversation-with-the-operator.md): messages, asks and
- operator messages; asks' kinds and life; the kinded benches `channel` and `intake`; the capability
- vocabulary; agents as participants; the migration.
-7. [The work context, and the desk](07-the-work-context-and-the-desk.md): the signals, the routing
- by context and its escalation, presence kept inside the mesh, and the desk as a full participant
- (notification actions, the launcher's prompt).
-8. [Asks that authorise](08-asks-that-authorise.md): the actions that need a person, the trust
- capabilities, the three proofs and the three tiers, the controller's checks, why the desk needs a
- factor, and what a compromise can reach.
-9. [A proposed decision](09-a-proposed-decision.md): the recommendation, the operator's steps, the
- tables, and a record ready for graduation.
diff --git a/01-RESEARCH/028-the-meshs-output-channel/01-what-the-mesh-already-knows.md b/01-RESEARCH/028-the-meshs-output-channel/01-what-the-mesh-already-knows.md
deleted file mode 100644
index 1dfd8e53..00000000
--- a/01-RESEARCH/028-the-meshs-output-channel/01-what-the-mesh-already-knows.md
+++ /dev/null
@@ -1,60 +0,0 @@
-# 01 — What the mesh already knows, and who hears it
-
-## The count
-
-Over the 236 issue reports in `04-ISSUES/`, on the day this effort opened:
-
-- **15** say, in some wording, that a person found the fault by looking: "nobody was told",
- "nothing logged / said / alerted / emitted", "a person asked", "found by a person". Five of them
- are still open.
-- **74** describe something that failed silently.
-
-The search was a word match over the reports' text, so it undercounts reports that tell the same
-story in other words. It never overcounts by much: each of the fifteen was read.
-
-The fifteen fall into three groups:
-
-- **The mesh knew, and kept it in a query.** The fault was in `status`, `plans` or a node's record,
- for whoever asked. Examples: a rollout waiting for ever (230), a machine refusing every declaration
- (233), a setting that cannot work stored and stopping the node (096).
-- **The fault was in a log nothing reads.** Examples: the bus refusing the controller's publishes
- (187), a dropped report (187, 230).
-- **The fault was invisible to the mesh itself.** Examples: a resolver outside the mesh closed by its
- filter (198), a port narrowed without saying (086).
-
-Only the first group is a matter of telling: the fact exists, and only delivery is missing. The
-other two need a source first. This effort is about the first, and about giving the other two a
-place to say something once they can.
-
-## What the controller computes and does not say
-
-Read from `status` and `node show` on the day this effort opened. Each line is a fact the controller
-already holds:
-
-| fact | where it is today | example that day |
-|---|---|---|
-| a machine is out of touch | `node show`: "last heard from — out of touch 10m" | a workstation after an upgrade |
-| a machine refused its declaration | `status`: "refused" with the reason | the same workstation, for 90 minutes |
-| a failure repeats and will not fix itself | `status`: "stuck: the same failure N times since …" | 13 times on the laptop |
-| machines run different hosts | `status`: the version table | after a host release |
-| something runs that the mesh did not write | `node show`: strays | 16 containers on one machine |
-| a filter rule the mesh did not write | `status` | one machine |
-| a plan is waiting | `plans` | issue 230: "for 0s", for ever |
-| an assignment does not compose | the `assign` answer only | issue 235 |
-
-None of these is published. The bus carries a seat's own events (a build's outcome), a module's
-declared events, tool calls and declarations. It carries no event for any line above.
-
-## What exists to deliver with
-
-- **A machine's desktop:** the `node-notifier` seat (ADR 0208), held on the laptop. Its `send` verb
- shows a notification, and `history` lists them. It was used through the console the day this effort
- opened.
-- **Mail:** a mail module provides `smtp` to the mesh.
-- **Chat:** a Matrix server runs as a module on the home server.
-- **Home automation:** a home-automation module runs there too, and its phone app can receive pushes.
-- **A seat shape for exactly this:** to-be 32 §5 uses `telegram-sender` (`accepts: send`,
- `retain 7d`, `emits: delivered, failed`, `serves: status`) as its worked example. A seat's stream
- exists from registration, so work queues until a holder appears.
-
-No module sends to Telegram, a phone push service or SMS today.
diff --git a/01-RESEARCH/028-the-meshs-output-channel/02-the-channels.md b/01-RESEARCH/028-the-meshs-output-channel/02-the-channels.md
deleted file mode 100644
index 14191267..00000000
--- a/01-RESEARCH/028-the-meshs-output-channel/02-the-channels.md
+++ /dev/null
@@ -1,116 +0,0 @@
-# 02 — The channels
-
-Each channel is a candidate module that delivers what the output seat hands it. They are weighed on
-the same questions:
-
-- **Reach:** does it reach the operator away from the machines (phone), or only at a desk?
-- **Off-mesh:** does it still work when the mesh's own parts (the bus, the controller, the control
- node's network) are what failed?
-- **Two-way:** can the operator answer through it: acknowledge, silence, ask?
-- **Where the words go:** does the message leave the operator's own machines, and to whom?
-- **What it costs to hold:** a secret, a server, an account, money.
-
-## The candidates
-
-### Telegram (required by the operator)
-
-A bot created with Telegram's bot service sends to one chat: the operator's own, or a group.
-
-- **Reach:** the phone and every desktop, with push.
-- **Off-mesh:** sending needs only outbound HTTPS from any machine. No inbound port, no server of the
- mesh's own. A second machine can hold the same bot token and send when the first is the one that
- failed.
-- **Two-way:** yes. Inline buttons on a message (acknowledge, silence for an hour) and commands to
- the bot, read by long polling over outbound HTTPS. This makes Telegram the strongest candidate for
- answering, and the riskiest (see [03](03-open-questions.md), Q7).
-- **Where the words go:** to Telegram's servers. Bot chats are not end-to-end encrypted. What a
- message may contain is therefore a rule this effort must set.
-- **Cost:** one secret (the bot token) and the chat's id. Free. Rate limits are far above what an
- operator should receive.
-- **Formatting:** short text with a little markup, buttons and links. Enough for a subject, a
- machine role, a severity and one line of why.
-
-### The desktop notifier (exists)
-
-The `node-notifier` seat's `send` verb on the machine the operator is at.
-
-- **Reach:** only at that machine, only while a session is up.
-- **Off-mesh:** no. It is reached through the mesh's tools.
-- **Two-way:** dunst has actions, which a click can answer, but nothing reads them back yet.
-- **Where the words go:** nowhere; it is local.
-- **Cost:** none.
-- **Its place:** the gentlest channel, for a warning while the operator is at a desk. "At a desk" is
- itself a question: an unlocked session on a machine with recent input.
-
-### ntfy (or Gotify): a self-hosted phone push
-
-A small server publishes topics; its phone app subscribes.
-
-- **Reach:** the phone, with push.
-- **Off-mesh:** only if the server runs outside what failed. On the control node it fails with it.
-- **Two-way:** action buttons can call a URL, which is an inbound path to design.
-- **Where the words go:** stays on the operator's machines when self-hosted. ntfy's iOS push passes
- through an upstream relay unless configured otherwise.
-- **Cost:** a module with a container and a routed name; a token per topic.
-
-### Matrix (a server exists as a module)
-
-A bot account posts to a room the operator is in.
-
-- **Reach:** phone and desktop through any Matrix client.
-- **Off-mesh:** no, the server is one of the mesh's modules.
-- **Two-way:** yes, by messages to the bot.
-- **Where the words go:** stays on the operator's server, end-to-end encrypted if the bot supports it.
-- **Cost:** a bot account, a secret.
-
-### Mail (a mail module provides `smtp`)
-
-- **Reach:** everywhere, without urgency.
-- **Off-mesh:** no, if the mesh's own mail server sends. Yes, through an outside relay.
-- **Two-way:** no, not usefully.
-- **Its place:** the record and the digest: a daily summary of what was said and resolved, and the
- fallback when nothing else acknowledged.
-
-### The home-automation companion app (a module exists)
-
-Its phone app takes pushes and actionable notifications, and the home has lights and speakers.
-
-- **Reach:** the phone, and the house itself: a light that turns a colour.
-- **Off-mesh:** no, the home server is a node.
-- **Its place:** a playful critical channel, not a primary one.
-
-### The bar on the desktop
-
-An `i3status-rust` block showing the count of open messages, red while one is critical.
-
-- **Reach:** the desk only, and silent.
-- **Its place:** the ambient state. Nothing interrupts the operator, and they always see whether
- something is open.
-
-### The console (an agent session)
-
-A message the next agent session opens with ("two things happened while you were away").
-
-- **Its place:** context for the agent working on the mesh rather than an alert. It falls out of the
- message store if the store is queryable.
-
-### Others, noted and not pursued now
-
-- **SMS or a voice call** through a paid gateway. It is the only channel that works with no data
- connection, and the only one that costs per message.
-- **Signal**, through an unofficial client: no bot API, and a registered number.
-- **Discord or Slack** webhooks: the words go to a third party, as with Telegram, without its two-way
- strength.
-- **Pushover:** paid, closed, and a phone push service much like ntfy.
-- **An external dead-man service** (a heartbeat URL that alerts when pings stop). It belongs to
- [03](03-open-questions.md), Q6, as the watcher's watcher rather than as a channel.
-
-## A first reading
-
-- **Telegram** is the primary phone channel, and the only candidate that is cheap, off-mesh capable
- and two-way at once.
-- **The desktop notifier** is for the desk.
-- **The bar** shows the ambient state.
-- **Mail** carries the digest and the record.
-- **ntfy and Matrix** are self-hosted alternatives for an operator who keeps words off third parties.
- The seat must make that a choice, not a rewrite.
diff --git a/01-RESEARCH/028-the-meshs-output-channel/03-open-questions.md b/01-RESEARCH/028-the-meshs-output-channel/03-open-questions.md
deleted file mode 100644
index 1d221575..00000000
--- a/01-RESEARCH/028-the-meshs-output-channel/03-open-questions.md
+++ /dev/null
@@ -1,128 +0,0 @@
-# 03 — Open questions
-
-Each question names the options seen so far. None is decided here. Q1 and Q7 are taken further in
-[06](06-a-conversation-with-the-operator.md) and [08](08-asks-that-authorise.md).
-
-## Q1. The seat
-
-**What speaks for the mesh to its operator?**
-
-- **a.** One seat in the mesh's own set, held once for the mesh. Working name: `operator-channel`.
- - It **accepts** `notify` (a work queue, as to-be 32 §5 designs `telegram-sender`), so a message
- waits until a holder appears.
- - It **emits** `delivered`, `acknowledged` and `resolved`.
- - It **serves** `open` (what is unresolved now) and `history`.
-- **b.** No seat: every source calls every channel. Rejected in advance, because each source would
- learn every channel. This is the inversion ADR 0126 exists to prevent.
-- **c.** Each channel as its own seat, with routing in the sources. Same objection as b, one level up.
-
-Under a, the holder routes. The channels are modules that **contribute** themselves to the seat
-(ADR 0210): a channel extends the seat, and so depends on it. Whether the holder is a module of its
-own or part of the controller is open. A module keeps the controller small. The controller already
-holds most of the facts.
-
-## Q2. What a message is
-
-The first shape seen: a **subject** (what it is about: a machine's role, a module, a plan), a
-**kind** (out of touch, refused, stuck, late, …), a **severity**, a one-line **why**, a link to
-the tool that shows more, and a **key** that makes it the same message the next time it is said.
-
-- **Severity:** two levels (needs you now / when you can), or three (critical / warning / info)?
- Every extra level is a routing rule somebody must keep right.
-- **The key** is what makes deduplication possible. "Machine X out of touch" said every minute is
- one message, still open, not sixty.
-
-## Q3. The life of a message
-
-open → (acknowledged) → resolved.
-
-- **Deduplicate** by key while open.
-- **Resolve** when the source stops saying it, or says it is over ("back in touch after 14 min"). A
- channel that can edit its message (Telegram can) updates it in place rather than sending a second.
-- **Acknowledge** from any channel that can answer, which stops escalation and repeats.
-- **Repeat or escalate** an unacknowledged critical message after a while, to the next channel.
-- **Where the open set lives:** the seat's own state, in a key-value bucket (to-be 32's `state:`), so
- `open` answers after a restart.
-
-## Q4. Routing and presence
-
-- **By severity:** critical goes to every channel at once. A warning goes to the desk when the
- operator is at one, otherwise to the phone, otherwise to the digest.
-- **Presence:** "at a desk" needs a fact the mesh does not hold yet. Candidates: an unlocked
- graphical session with recent input, read from the `node-lock-screen` and `node-login-manager`
- seats' holders. Nothing more invasive.
-- **Quiet hours:** a setting of the seat's holder (ADR 0174). Critical overrides it, or not, as the
- operator chooses.
-- **Rate:** a cap per hour per channel, with the excess folded into one summary, so a storm (a
- network outage where every node is out of touch) arrives as one message naming many.
-
-## Q5. The sources
-
-The first sources are the facts in [01](01-what-the-mesh-already-knows.md), all in the controller
-today:
-
-- a machine out of touch;
-- a declaration refused;
-- a stuck failure;
-- a plan late (once issue 230 gives a wait an age);
-- an assignment that does not compose (issue 235);
-- a host version split.
-
-**How each becomes an event:**
-- **a.** The controller emits an event per change of state, and the seat's holder consumes them.
-- **b.** The controller calls `notify` itself.
-
-With a, the controller learns nothing about telling: other consumers (a board, a log) get the same
-facts, and the holder decides what is worth a message. With b, the controller decides severity.
-
-**Modules as sources:** a module may `use` the seat to tell the operator something of its own
-(a backup failed, a certificate is close to expiry), with the same message shape.
-
-## Q6. The watcher's watcher
-
-When the controller, the bus or the control node is what failed, nothing above runs. Options:
-
-- **A dead-man signal:** the seat's holder sends a heartbeat out of the mesh (a ping to an external
- heartbeat service), which alerts the operator by its own means when pings stop.
-- **A second holder of the Telegram channel on another machine** that sends directly, without the
- bus, when it stops hearing the controller for longer than a bound.
-- **Each host** sending a last message itself when it loses the mesh for longer than a bound. This
- needs the channel's secret on every machine, a cost to weigh.
-
-The first is the cheapest and the only one that also covers "the whole house is offline".
-
-## Q7. Answering back
-
-Telegram, and Matrix, can carry the operator's answers.
-
-- **Acknowledge and silence** are safe: they change only the message's state.
-- **Commands** ("push the workstation", "show status") turn a chat account into a door to the
- controller. If it ever comes, it needs:
- - its own record;
- - a narrow verb set;
- - a check that the answer came from the operator's own account and chat;
- - and probably a confirmation step.
-
-The first version should probably answer with acknowledge and silence only.
-
-## Q8. What may leave the mesh
-
-Telegram, and any third-party channel, carries the words to someone else's servers. A message
-names a machine, a module and a reason, which is operational detail.
-
-- **What may a message contain?** Roles rather than addresses; no secrets, tokens or paths; a reason
- in words. The rule must be enforced by the seat's holder, not hoped for from each source.
-- **Is a self-hosted channel required for anything above a severity?**
-- **The bot token and chat id** are secrets of the channel's module, delivered as any module secret is.
-
-## Q9. How it is checked
-
-A rule this effort produces must say how it is verified. Candidates:
-
-- a message said twice with one key is one message;
-- a resolved source resolves its message;
-- a critical message reaches every channel within a bound;
-- a message containing an address or a secret is refused;
-- the dead-man signal fires when the holder is stopped.
-
-Each is a test of the holder, or a live drill: stop a machine's host and time the message.
diff --git a/01-RESEARCH/028-the-meshs-output-channel/04-telegram-as-the-first-holder.md b/01-RESEARCH/028-the-meshs-output-channel/04-telegram-as-the-first-holder.md
deleted file mode 100644
index b21bc04e..00000000
--- a/01-RESEARCH/028-the-meshs-output-channel/04-telegram-as-the-first-holder.md
+++ /dev/null
@@ -1,222 +0,0 @@
-# 04 — Telegram, as the first holder of a channel
-
-Telegram was the operator's first required channel ([02](02-the-channels.md)) and it is built:
-the output seat's holder carries a Telegram client, and so does the watcher's watcher (to-be 45 §5).
-Neither is configured, because no bot exists yet. This document is what the operator needs to make one,
-what the mesh's use of the bot API must respect, and what the built code gets wrong against it.
-
-[06](06-a-conversation-with-the-operator.md) makes Telegram one holder of a generic channel seat rather
-than the subject of the design. Everything here stays true under that shape: it is the first holder's
-analysis.
-
-Facts are as of 2026-10-06, Bot API 10.3 (2026-08-24). Sources are listed at the end.
-
-## What the mesh uses from Telegram
-
-Two programs send, and neither reads anything back yet:
-
-- **The output seat's holder**, on the control node. It sends a message when a condition is raised,
- says it again as a reminder, and edits the first message in place when the condition clears.
-- **The watcher's watcher**, on a machine that is not the control node. It sends straight to the bot
- API over HTTPS when the controller's self-check or the bus has been silent past its bound.
-
-Each holds a **bot token** as its own secret, issued outside the mesh (ADR 0228, `issued-by: outside`),
-and a **chat id** as a setting. Each sends plain text: no `parse_mode`, so no markup to escape and none
-to inject.
-
-## Making the bot
-
-Telegram has no developer console. A bot is made by talking to Telegram's own bot, BotFather, from an
-ordinary Telegram account.
-
-- **An account is required, and an account needs a phone number.** There is no other sign-up. The
- number can be a virtual one bought on Telegram's own marketplace, at a price that makes it
- irrelevant here.
-- **`/newbot`** asks for a display name and a username. The username is 5–32 characters of Latin
- letters, digits and underscores, must end in `bot`, and cannot be changed later.
-- BotFather answers with the **token**: digits, a colon, then a key. Anyone holding it controls the bot.
-- **`/token`** issues a new token for the bot. The old one stops working at once. This is the rotation
- path, and it is the only one.
-- **`/setjoingroups` → Disable** stops anyone adding the bot to a group. The mesh's bot talks to one
- person; a group is only a way for someone else to see what it says.
-- **Privacy mode** (`/setprivacy`) governs what a bot sees **in groups**: with it on, only commands
- meant for it, replies to it and service messages. In a private chat a bot sees everything the person
- writes. With groups disabled, privacy mode does not matter; leave it on.
-
-### A bot cannot speak first
-
-A bot cannot open a conversation. Until the person presses **Start** in the bot's chat, every send to
-them fails with a "Forbidden" error. So the operator presses Start once, on each bot.
-
-### Finding the chat id, safely
-
-In a private chat the chat id equals the person's user id. There are two ways to learn it:
-
-- **Read `getUpdates` once by hand.** After pressing Start, a call to `getUpdates` returns the `/start`
- message with the chat's id. It works today. Its two weaknesses: the token appears in a command line
- (and so in a shell's history) unless read from a file, and it trusts that the `/start` it sees is the
- operator's. A bot's username is public, and anyone who finds it can press Start too.
-- **A linking verb with a one-time code.** The holder makes a short code and answers with a deep link
- (`https://t.me/?start=`). The operator opens it on the phone; Telegram sends `/start `.
- The holder reads it by `getUpdates`, and binds **that** chat and **that** user id only if the code
- matches and is fresh. This proves the chat belongs to whoever held the code, and it never shows the
- token to anyone. It needs the holder to read updates, which approvals need anyway
- ([08](08-asks-that-authorise.md)).
-
-The second is the one to build. The first is the stop-gap until it exists.
-
-### One bot or two
-
-The holder and the watcher each have their own secret. They can hold the same token or two.
-
-**Two bots are better:**
-- Revoking one does not silence the other. The watcher exists for the day the rest is broken, and
- that day must not also be the day its token was rotated away.
-- The phone shows which program spoke.
-- **Only one program may read a bot's updates.** Two concurrent `getUpdates` callers on one token make
- Telegram answer the older with HTTP 409, "terminated by other getUpdates request". The moment the
- holder reads answers, the watcher could no longer share its token with anything that reads.
-
-## The bot API, as the mesh uses it
-
-### Limits
-
-- **Rate.** Telegram's FAQ: "In a single chat, avoid sending more than one message per second." In a
- group, 20 messages a minute. Broadcast across chats: about 30 a second. The holder's own cap is 20 an
- hour, so the limit is never near.
-- **Over the limit** the API answers HTTP 429 with `parameters.retry_after`, the seconds to wait
- before the request may be repeated. Repeating early prolongs the wait.
-- **Length.** A text message is at most 4096 characters after entity parsing. Longer is refused with
- HTTP 400, not cut.
-- **Callback data** on a button is 1–64 bytes ([08](08-asks-that-authorise.md)).
-
-### Editing
-
-- **`editMessageText`** replaces a sent message's text. For an ordinary bot message there is no time
- limit. The 48-hour limit applies only to business messages, and deletion has its own 48-hour limit.
-- **An edit notifies nobody.** No sound, no banner, and the message stays where it was in the chat's
- history. This is why a clearing is cheap to say by edit. It is also why an edit must never be the
- only way something **new** is said.
-- **An identical edit is an error:** HTTP 400, "message is not modified". It is harmless and must be
- read as success, not as a failure to fall back from.
-- **A deleted message** answers "message to edit not found". Falling back to a new message is right
- then.
-
-### Loudness
-
-Telegram has **no message priority**. The only lever is **`disable_notification`**: the message
-arrives without sound. It cannot break through the phone's do-not-disturb, so an urgent message at
-night is as quiet as the phone is set to be. Per-chat notification settings on the phone (a custom
-sound, an exception to do-not-disturb) are the operator's, not the mesh's.
-
-### Formatting
-
-With no `parse_mode` the text is shown as written, and nothing in a message can be read as markup.
-That is the right default for words that pass a content rule rather than a template. If markup is ever
-wanted, `MarkdownV2` needs every reserved character escaped and fails the whole send on one miss, so
-plain text or `HTML` with escaping are the safer options.
-
-`disable_web_page_preview` was **deprecated in Bot API 7.0** in favour of
-`link_preview_options: {is_disabled: true}`. It still works, but the mesh's messages carry no links
-(the content rule refuses URLs), so the parameter can simply be dropped.
-
-### Answering back
-
-Answers reach a bot two ways:
-- **Long polling with `getUpdates`**, over outbound HTTPS. Updates wait at Telegram for at most
- 24 hours.
-- **A webhook** (`setWebhook`), which Telegram calls over HTTPS on port 443, 80, 88 or 8443. It may
- carry a secret header (`X-Telegram-Bot-Api-Secret-Token`) that proves the call came from the webhook
- that was set.
-
-Only one of the two at a time. [08](08-asks-that-authorise.md) chooses between them.
-
-## What Telegram sees
-
-- **Everything in the message.** A bot chat is a "cloud chat": encrypted between the phone and
- Telegram, and between Telegram and the bot API caller, and readable by Telegram. Bots cannot take
- part in Telegram's end-to-end "secret chats".
-- **That is what the content rule is for.** The holder refuses any message carrying an address, a
- path or a secret's shape (to-be 45 §5), so what Telegram stores is roles, words and condition keys.
-- **Who the operator is.** The account's phone number, and the addresses the phone and the sending
- machines connect from. Since September 2024 Telegram's privacy policy says it may disclose a user's
- IP address and phone number to judicial authorities on a valid order.
-- **Machine names.** A condition key names the machine it is about, and so does the watcher's message
- ("told by mesh-watcher on …"). The content rule refuses host names with a top-level domain, not bare
- machine names. To-be 45 says a message's subject is "a machine's role". Whether a bare machine name
- may leave is a decision this effort has not taken; today it does.
-
-## When Telegram is unreachable
-
-- **A send fails at the transport** (no DNS, no connection, timeout). The holder keeps what it held
- and tries again every minute. The watcher keeps what it owes and tries again at its next tick.
- Neither loses a message while it runs.
-- **A send fails because Telegram refuses** (400 or 403). It is permanent for that message. The holder
- today treats it like a transport failure and tries again every minute, for ever (see the defects).
-- **Telegram being down is invisible to Telegram.** The holder's status says the channel is failing.
- The operator sees that only through another channel or by asking. This is what a second holder of a
- different kind is for ([05](05-the-other-holders-on-the-same-axes.md)).
-- **Telegram is blocked** in some countries and on some networks. An operator travelling should know
- the mesh's phone channel may be one of them.
-
-## Cost
-
-- **Free.** No per-message charge. Telegram's paid broadcasts (above 30 messages a second) are far
- out of range.
-- **One account**, which the operator very likely already has.
-- **Two secrets**, one token per bot, both `issued-by: outside`.
-
-## The built code, checked against this
-
-Read from the code repository's main branch on 2026-10-06: the holder's `telegram.go`, `outbox.go`,
-`holder.go`, `content.go`, and the watcher's `telegram.go` and `watcher.go`. The two Telegram clients
-are copies of each other, kept apart on purpose so the watcher depends on nothing it watches.
-
-What is right:
-- **Plain text**, with no `parse_mode`.
-- **The token is kept out of every error.** The client rebuilds transport errors from their kind,
- because the URL carries the token. It also strips the token from the API's own description.
-- **Token and chat id are re-read at each send**, so accepting the secret or changing the setting needs
- no restart.
-- **A token's shape is checked.** A random value the mesh minted for an un-accepted secret is named as
- that, not sent to Telegram to be refused.
-- **A missing edit falls back to a new message.**
-- **The holder caps itself** at 20 messages an hour and folds bursts into digests, far inside
- Telegram's limits.
-
-### Defects
-
-| # | Where | What | Effect | Weight |
-|---|---|---|---|---|
-| D1 | holder: `telegram.go`, `outbox.go` | Nothing bounds a message to 4096 characters. A long summary, or a digest of long titles, is refused with 400. `failed` keeps the whole batch and retries it every minute. | One oversized message wedges the Telegram channel: everything queued behind it waits for ever. | high |
-| D2 | holder: `holder.go` (`h.edit(old, "reopened", false)`) | A condition that clears and is raised again within ten minutes is said by **editing** the first message. On Telegram an edit notifies nobody. | A reopened urgent condition reaches the phone **silently**, high up in the chat's history. | high |
-| D3 | holder: `outbox.go` (`r.Sent[name]` keeps only the first id) | Reminders and escalations are new messages, but clearing edits only the first. | The newest thing on the phone still says "STILL OPEN" or "NOW URGENT" after the condition cleared. The "CLEARED" is a silent edit, out of sight. | medium |
-| D4 | both: `telegram.go` | `Message.Quiet` and `Message.Urgent` are ignored. `disable_notification` is never set. | A clearing, or a warning, rings as loudly as an urgent message. Telegram's only loudness lever is unused. | medium |
-| D5 | both: `telegram.go` (`call`) | HTTP 429's `parameters.retry_after` is not read. The retry is a flat minute. | Harmless at the holder's cap. Under a real flood wait, repeating early prolongs it. | low |
-| D6 | holder: `telegram.go`, `outbox.go` | Every refusal (400, 403) is retried like a transport failure. "message is not modified" on an edit is read as a failed edit, and a new message is sent instead. | Permanent errors loop every minute in the log. A no-op edit becomes a duplicate message. | low |
-| D7 | both: `telegram.go` | `disable_web_page_preview` is deprecated since Bot API 7.0. | Works today. Moot, since no message carries a link. Drop it. | low |
-| D8 | both: `telegram.go` (`call`) | The `json.Marshal` error is discarded. A non-numeric `message_id` (`json.Number`) makes the body empty. | A confusing refusal from Telegram instead of a local error. Ids come from Telegram, so it is unlikely. | low |
-| D9 | watcher: `watcher.go` (`Tick`) | A "silent" message that could not be sent is overwritten by the "CLEARED" message when the signal returns. | The operator can receive "heard again" for a silence they were never told of. Better to say both, or one line saying it was silent for N minutes and is back. | low |
-| D10 | both | `Ready()` is satisfied by a token and a chat id. It does not know whether the operator pressed Start, or whether the bot was blocked (403). | Status says "ready" until the first send fails. The watcher's own test verb is the only proof. A `getChat` check at status time would say it. | low |
-
-D1 and D2 matter before the channel is configured. D1 can silence the channel. D2 silences exactly the
-case (a flapping urgent condition) the operator most needs to hear.
-
-## Sources
-
-- Telegram, *Bots FAQ*: rate limits, paid broadcasts. https://core.telegram.org/bots/faq
-- Telegram, *Bot API* (version 10.3, recent changes, `getUpdates` retention, `ResponseParameters`,
- `setWebhook`, `link_preview_options`). https://core.telegram.org/bots/api
-- Telegram, *Bot features*: BotFather, `/newbot`, `/token`, `/setprivacy`, deep linking.
- https://core.telegram.org/bots/features
-- `link_preview_options` replacing `disable_web_page_preview` (Bot API 7.0); the removal of the old
- argument in python-telegram-bot v22. https://docs.python-telegram-bot.org/en/v22.0/telegram.ext.defaults.html
-- "message is not modified" and "message to edit not found" in practice:
- https://github.com/tdlib/telegram-bot-api/issues/400
-- 409 "terminated by other getUpdates request":
- https://community.home-assistant.io/t/help-on-telegram-extension-error-while-getting-updates-conflict-terminated-by-other-getupdates-request-make-sure-that-only-one-bot-instance-is-running-409/177544
-- Telegram privacy policy change, September 2024:
- https://www.bleepingcomputer.com/news/security/telegram-now-shares-users-ip-and-phone-number-on-legal-requests/
-- Bots and secret chats; cloud-chat encryption:
- https://www.kaspersky.com/blog/telegram-privacy-security/38444/
-- Phone number required; anonymous numbers: https://en.wikipedia.org/wiki/Telegram_(software)
diff --git a/01-RESEARCH/028-the-meshs-output-channel/05-the-other-holders-on-the-same-axes.md b/01-RESEARCH/028-the-meshs-output-channel/05-the-other-holders-on-the-same-axes.md
deleted file mode 100644
index 102b6b98..00000000
--- a/01-RESEARCH/028-the-meshs-output-channel/05-the-other-holders-on-the-same-axes.md
+++ /dev/null
@@ -1,186 +0,0 @@
-# 05 — The other holders, on the same axes
-
-Every candidate is judged as a holder of the channel and intake seats in
-[06](06-a-conversation-with-the-operator.md): which capabilities it can honestly declare
-(the vocabulary is defined there), and what it needs. [02](02-the-channels.md) weighed the same
-candidates before any of this was measured. This document replaces its reading of them with facts as
-of 2026-10-06, and adds the question 02 could not ask: **can the operator answer through it, and
-authorise an action through it?** ([08](08-asks-that-authorise.md)).
-
-Two roles are judged separately, because they want different things:
-
-- **The away channel:** the mesh's urgent messages and its asks, wherever the operator is.
-- **The watcher's path:** the message that the mesh itself has gone silent. It must not depend on the
- control node, the bus or the controller. The mesh observed has its controller, its bus and its mail
- server on the anchor (the control node). Its Matrix server is on the home-server, behind a household
- connection.
-
-## The candidates
-
-### ntfy
-
-A small push server. Topics are published to over HTTP; the phone app subscribes.
-
-- **Self-hosted vs the public server.** Self-hosted keeps the words on the operator's machines.
- The public `ntfy.sh` takes no sign-up. Its free tier allows 250 messages a day **per IP address**,
- shared with whoever else sends from that address. Paid tiers (from about $5–6 a month) give
- reserved topics and higher quotas. An unreserved topic on the public server is readable by anyone
- who guesses its name.
-- **Phone delivery.**
- - **Android:** through Google's FCM from the public server, or through the app's own long-lived
- connection to a self-hosted server ("instant delivery"), which costs battery.
- - **iOS:** cannot be reached by a self-hosted server alone. The server must name an upstream
- (`upstream-base-url`, normally `ntfy.sh`), which receives a poll request carrying only a message
- id and a hash of the topic, and has Apple wake the phone. The words do not pass through the
- upstream. The dependency does.
-- **Loudness:** five priorities. The highest gives "really long vibration bursts" and a pop-over on
- Android.
-- **Answers:** up to three action buttons. An `http` action makes **the phone** send a request,
- which needs a route from the phone to the mesh and a credential carried inside the notification.
- Nothing tells the server **who** tapped, only that someone holding the notification did. No free-text
- reply.
-- **As the watcher's path:** self-hosted, it fails with the machine it runs on. The public server
- works, at the cost of a guessable topic or a subscription.
-
-### Matrix (a homeserver is already one of the mesh's modules)
-
-The module runs Conduit and Element Web, on the home-server.
-
-- **Reach:** any Matrix client on the phone. Push goes from the homeserver to the client's **push
- gateway**: for the stock Element apps, Element's gateway at matrix.org, which hands it to Apple or
- Google. A self-hosted gateway needs a self-built app. UnifiedPush (via ntfy) is an option on Android.
-- **Push support in Conduit has lagged.** Its own documentation long listed mobile push as missing,
- and forks have since reworked pushers. Whether the running version pushes reliably is **unverified**
- and must be measured before Matrix is relied on for anything urgent.
-- **Answers:** free text, and reactions (`m.reaction` annotations) as one-tap choices. Clients add
- emoji variation selectors, which must be normalised before a reaction is read as a choice.
-- **Who answered:** the sender's Matrix id is authenticated **by the homeserver**, which the mesh
- runs. That is strong for an account on the mesh's own server, and only as strong as that server.
-- **Privacy:** end-to-end encrypted if the bot supports it. Otherwise readable by the homeserver,
- which is the mesh's own.
-- **As the watcher's path:** it survives the control node going down. It does not survive the home's
- connection going down, and its phone push still depends on matrix.org's gateway.
-- **Cost:** a bot account and its secret. No third party for the words.
-
-### Pushover
-
-A paid push service with a stable API.
-
-- **Cost:** $4.99 one-time per platform after a 30-day trial. 10,000 messages a month per
- application.
-- **Limits:** 1024 characters, a 250-character title.
-- **Loudness:** the strongest of any candidate.
- - Priority 1 bypasses the user's quiet hours.
- - Priority 2 ("emergency") repeats every `retry` seconds (at least 30) until acknowledged or until
- `expire` (at most three hours). It returns a **receipt** that can be polled outbound to learn
- whether, and when, it was acknowledged.
-- **Answers:** acknowledgement only. No buttons, no reply.
-- **As the watcher's path:** yes. It is outbound HTTPS from any machine, to a third party.
-- **Where the words go:** to Pushover.
-
-### Gotify
-
-Self-hosted, Android only. Delivers over a WebSocket the app keeps open. There is no official iOS app,
-and Apple's restrictions make a self-hosted iOS push impossible without a relay. It has no answer path
-beyond opening the app. **Not pursued:** it covers less than ntfy and nothing ntfy does not.
-
-### Mail through an outside provider
-
-- **The mesh's own mail server is on the control node,** so it fails with it.
-- **An outside relay** (an SMTP account at a mail provider) reaches the operator from any machine.
-- **Urgency:** none. Mail is the digest and the record.
-- **Answers:** a reply, slowly. **Who answered is weak:** a From line can be forged, and checking
- DKIM only proves the operator's provider sent it.
-- **Mail as an intake** (a new mail arriving) is a trigger in its own right
- ([06](06-a-conversation-with-the-operator.md)), whatever its weakness as a channel for asks that authorise.
-
-### Signal, through `signal-cli`
-
-- **An unofficial client,** and it needs its own phone number, registered with a captcha.
-- **It must be kept current.** Signal's own clients expire after three months, and the server then
- changes incompatibly. In March 2026 Signal began unregistering accounts whose client lacked a new
- protocol feature, and every `signal-cli` account registered before that date was dropped.
-- **Privacy:** end-to-end encrypted. Sender identity is strong (Signal's identity keys). Reactions
- and replies both work.
-- **Weight:** a phone number and a maintenance burden with a hard failure mode. It is the right
- choice only for an operator who requires end-to-end encryption on the phone and accepts that burden.
-
-### SMS through a paid gateway
-
-The only candidate that reaches a phone with **no data connection**.
-
-- **Cost:** per message, with an account at a gateway.
-- **Privacy:** no encryption. Sender identity on replies is spoofable.
-- **Its place:** the last resort of an outside dead-man service (below), which offers SMS and phone
- calls on paid plans, rather than a channel of the mesh's own.
-
-### An outside dead-man service
-
-A service the mesh **pings**, which alerts by its own means when the pings stop. It is not a channel
-of the mesh: it is the one thing that still speaks when **every** machine, or the home's connection
-and the anchor together, are gone. [03](03-open-questions.md) Q6 named it the cheapest answer that
-also covers "the whole house is offline".
-
-- **Healthchecks.io** (also self-hostable, which defeats the point here) monitors 20 checks free,
- without a card.
-- It notifies through Telegram, Signal, Matrix, ntfy, Pushover, mail and others.
-- Paid plans add SMS, WhatsApp and phone-call credits.
-
-## The table
-
-Capabilities are those of [06](06-a-conversation-with-the-operator.md). ✓ declared honestly,
-— not, ~ conditional (the note says on what).
-
-| Holder | reaches-away | loud | silent | edit | choice | reply | verified-sender | exact-render | code-factor | private | off the control node | cost |
-|---|---|---|---|---|---|---|---|---|---|---|---|---|
-| Telegram | ✓ | — (do-not-disturb wins) | ✓ | ✓ | ✓ | ✓ | ✓ (user id) | ✓ | ✓ (code as a reply) | — | ~ (a holder on another machine) | free |
-| desktop notifier | — | ~ (critical urgency) | ✓ | ✓ | ~ (actions, read by nobody) | — | — (any program of the account) | ✓ | — | ✓ | — | none |
-| ntfy, self-hosted | ✓ | ✓ (priority 5) | ✓ | — | ~ (http action, phone → mesh) | — | — | ✓ | — | ✓ (iOS: relay sees ids) | — | a module |
-| ntfy.sh | ✓ | ✓ | ✓ | — | ~ | — | — | ✓ | — | — | ✓ | free (250/day/IP) or ~$5/mo |
-| Matrix (own server) | ~ (push unverified) | — | ✓ | ✓ | ✓ (reactions) | ✓ | ✓ (own homeserver) | ✓ | ✓ | ~ (E2E if the bot does it) | ~ (home-server, not anchor) | a bot account |
-| Pushover | ✓ | ✓✓ (emergency, repeats) | ✓ | — | ~ (acknowledge only) | — | ✓ (for acknowledge) | ✓ | — | — | ✓ | $4.99 once |
-| mail, outside relay | ✓ (slow) | — | ✓ | — | — | ✓ | — (forgeable) | ✓ | ~ (code in a reply) | — | ✓ | an account |
-| Signal (`signal-cli`) | ✓ | — | ✓ | ✓ | ✓ (reactions) | ✓ | ✓ | ✓ | ✓ | ✓ | ~ | a number + upkeep |
-| SMS gateway | ✓ (no data needed) | ✓ | — | — | — | ~ | — | ✓ | — | — | ✓ | per message |
-
-## Reading it
-
-**For the away channel and for asks,** Telegram is the only candidate that is all of these at
-once: free, on both phone platforms, without a server of the mesh's own, and able to carry every tier
-of authorising ask ([08](08-asks-that-authorise.md)), including a code as a second proof. Its price is that
-Telegram reads the words, which is what the content rule is for.
-- **Matrix** is the self-hosted equivalent for asks. It waits on a measurement of Conduit's push.
-- **Signal** is the end-to-end-encrypted equivalent, at a maintenance cost that has already broken
- every installation once this year.
-
-**For waking the operator,** Pushover's emergency priority is the only thing that repeats until
-acknowledged and gets through quiet hours. Telegram cannot. It is a reasonable **second** away holder
-for urgent conditions only, if the operator wants to be woken. It cannot carry an answer beyond
-"acknowledged".
-
-**For the watcher's path,** the requirement is independence from what it watches:
-- **Telegram, sent directly** from a machine that is not the control node, with its own bot, meets it
- (to-be 45 §5).
-- **ntfy self-hosted does not.**
-- **Matrix only half meets it** (it is on the home-server, and its push goes through matrix.org).
-- **What none of them covers** is the watcher's own machine, or the home's connection, going down
- together with the anchor. Only an outside dead-man service covers that.
-
-## Sources
-
-- ntfy, *Configuration* (iOS upstream relay, FCM, access control): https://docs.ntfy.sh/config/
-- ntfy, *Publishing* (priorities, actions, `http` action, message size): https://docs.ntfy.sh/publish/
-- ntfy.sh pricing: https://ntfy.sh/#pricing. Its free-tier rate limit is per IP:
- https://github.com/binwiederhier/ntfy/issues/1963
-- Pushover API (length, quota, priorities, emergency retry/expire, receipts): https://pushover.net/api
-- Pushover pricing: https://pushover.net/pricing
-- Gotify, platform support: https://play.google.com/store/apps/details?id=com.github.gotify and
- https://guancyxx.cn/en/blog/ntfy-vs-gotify-vs-nostr
-- Element push gateway (Sygnal at matrix.org):
- https://github.com/vector-im/element-android/blob/develop/docs/notifications.md
-- Matrix reactions (`m.annotation`): https://github.com/uhoreg/matrix-doc/blob/aggregations-reactions/proposals/2677-reactions.md
-- Conduit changelog: https://conduit.rs/changelog/
-- signal-cli, registration with a captcha: https://github.com/AsamK/signal-cli/wiki/Registration-with-captcha
-- signal-cli, unregistration of outdated clients in 2026: https://github.com/AsamK/signal-cli/issues/1993
-- Healthchecks.io pricing: https://healthchecks.io/pricing/. Its Telegram integration:
- https://healthchecks.io/integrations/telegram/
diff --git a/01-RESEARCH/028-the-meshs-output-channel/06-a-conversation-with-the-operator.md b/01-RESEARCH/028-the-meshs-output-channel/06-a-conversation-with-the-operator.md
deleted file mode 100644
index 2fa0a454..00000000
--- a/01-RESEARCH/028-the-meshs-output-channel/06-a-conversation-with-the-operator.md
+++ /dev/null
@@ -1,332 +0,0 @@
-# 06 — A conversation with the operator
-
-The operator's directions, 2026-10-06, in substance:
-
-- **Telegram is one output channel among many to come.** The setup must be generic, and Telegram
- simply fulfils a seat.
-- **The same holds for input:** a new mail, a new message from the operator.
-- **Each channel has capabilities.**
-- **Approval is only an example.** An agent may just as well want to ask a simple question, and "it
- doesn't have to be permission related".
-
-So the core of this effort is not a notifier and not an approval path. It is **a conversation with
-the operator, held over channels**:
-
-- the mesh, its modules and its agents **say** things and **ask** things;
-- the operator **answers**, or **writes first**;
-- each exchange goes over whichever channel is right for its needs and for where the operator is.
-
-This document is that general model. [07](07-the-work-context-and-the-desk.md) is how the work context
-chooses the channel. [08](08-asks-that-authorise.md) is one layer on top: asks whose answer performs an
-action, and the checks that makes necessary. [04](04-telegram-as-the-first-holder.md) and
-[05](05-the-other-holders-on-the-same-axes.md) are the first holders.
-
-## What exists, and how it is shaped
-
-Read from the catalogue's main branch on 2026-10-06.
-
-- **The output seat's holder is one module doing three jobs.** It claims `operator-channel` (mesh
- scope, serving `open`, `history` and `notify`). It consumes the controller's three condition events.
- It holds the Telegram bot token as its own secret. It reaches the desktop through the `node-notifier`
- seat's `send`.
- - The router, the Telegram client and the desktop adapter are one process.
- - `notify` is a served verb, not the work queue to-be 32 §3 sketched.
-- **The desktop notifier** is the node seat `node-notifier`, held by the dunst module on each
- graphical machine.
-- **The watcher's watcher** has its own Telegram client and token, and is not assigned yet.
-- **Nothing reads anything back.** The operator speaks to the mesh only through an agent session or a
- shell.
-
-## The three things said
-
-- **A message:** the mesh tells the operator something. A condition raised, a reminder, a clearing,
- a notice from a module. It expects no answer. It may be edited later (a clearing).
-- **An ask:** someone wants the operator's input, of a declared kind. The answer goes back to whoever
- asked.
-- **An operator message:** the operator writes first. It is a message to an agent, a note to the mesh,
- or an answer to an ask written in the thread instead of tapped.
-
-Inputs from outside that are not the operator (a mail arriving, a webhook) are the same kind of
-envelope as an operator message, with a sender that is not the operator. This effort designs their
-shape only. Their consumers are later work.
-
-## Asks
-
-### Kinds
-
-| Kind | The operator gives | Required capabilities of the channel |
-|---|---|---|
-| `yes-no` | yes or no | `choice`, or `reply` (read as yes or no) |
-| `one-of` | one of up to eight labelled options | `choice`, or `reply` with the option's number |
-| `text` | free text | `reply` |
-| `number`, `date` | a value of that type, within bounds | `reply`. Parsed and checked by the seat's holder; asked again once if it does not parse. |
-| `acknowledge` | "seen" | `choice` |
-
-An ask whose answer **performs an action** (approve a retirement, delete data) is the same ask with
-an **authorising** flag. It adds requirements of trust, which [08](08-asks-that-authorise.md)
-defines. Everything else about it is as below.
-
-### What an ask carries
-
-- an id;
-- the asker (its bus principal and its machine);
-- the kind, with its options or bounds;
-- the words;
-- a priority (urgent or normal);
-- optionally a timeout and a default;
-- optionally a conversation handle (where the asker is talking with the operator);
-- optionally a group, for batching.
-
-The words pass the content rule like every message.
-
-### Its life
-
-**open → answered | defaulted | expired | cancelled**
-
-- **Answered:** the first answer wins. Copies of the ask shown on other channels are edited to say
- where it was answered.
-- **Defaulted:** the timeout passed and the ask declared a default. The asker receives the default,
- marked as a default, never as the operator's answer.
-- **Expired:** the timeout passed with no default. The asker is told.
- - **An authorising ask never defaults to performing.** It expires. ADR 0230's rule, "a timer is the
- mesh acting alone again, only later", applies to every ask that authorises.
-- **Cancelled:** the asker no longer needs it (`ask cancel`), or its owner sees it is moot (the
- condition behind it cleared). Its copies are edited to "no longer needed".
-
-### How the asker gets the answer
-
-- The answer is emitted as **`ask-answered`**, with the ask's id and, where there is one, the
- conversation handle.
-- An asker may **wait**: `ask` with a wait of up to a few minutes returns the answer if it comes in
- time, and otherwise returns the id.
-- An agent working through a long task can **poll** `asks `.
-
-### Batching
-
-- **Asks to the same channel within the burst window go out together.** A heading says how many are
- open ("3 questions waiting"), and each ask follows as its own message, so each can be answered and
- edited alone.
-- **An asker may hold at most three open asks.** A fourth is refused to it, in words, so an agent in
- a loop cannot flood the operator.
-- **Asks count against the router's hourly cap** like messages. Answers do not.
-
-### History
-
-**`asks`** lists open asks, and closed ones for 30 days: the asker, the kind, the outcome, the channel,
-and the answer. A free-text answer is the operator's own words. It stays in the router's state and is
-never forwarded to a channel other than the one it came from.
-
-## Who holds what
-
-### The output seat's holder becomes the conversation's router
-
-It holds `operator-channel` and gains asks:
-
-- `ask` (create),
-- `ask cancel`,
-- `asks`,
-- `answer` (called by intake holders, below),
-- events `ask-opened`, `ask-answered`, `ask-closed`.
-
-It **owns** every ask that authorises nothing.
-
-An authorising ask is **owned by the controller** ([08](08-asks-that-authorise.md)). The router carries
-it like any other, but its answer goes to the controller, which alone can perform.
-
-### Where a channel sits: [03](03-open-questions.md) Q1, asked again
-
-Q1 settled that **one seat speaks for the mesh** and that sources never learn channels. It assumed
-each channel attaches by contribution. With many channels to come, and channels that answer, that is
-the question to settle.
-
-The mesh's precedents:
-
-- A **seat** has one holder at its scope (ADR 0121, ADR 0126).
-- A **node seat** has one holder per machine, and a verb's subject carries the machine (design 33 §4).
-- A **bench** is a seat with several holders. The only one is `mesh-dns-resolver`: the same module,
- one per machine. "Making another one is a decision, recorded" (ADR 0223).
-- A **work queue** is shared by a seat's holders (ADR 0190).
-- A **contribution** is content another module hands to a seat's holder (ADR 0210, ADR 0212).
-
-The options:
-
-- **a. Each channel contributes itself to the output seat** (Q1 a, to-be 45 §5).
- - A contribution is content a holder places.
- - A channel is running code: it holds a secret, keeps a connection, reads answers, fails on its own.
- - Making it fit puts every channel's client back in the router. **Rejected.**
-- **b. One seat per channel kind.** The router learns every seat; a new channel is a change to the
- router. **Rejected.**
-- **c. Channel modules found by a manifest field and called by module address.** Callers use seats,
- never modules (ADR 0126). **Rejected.**
-- **d. One monolithic notifier,** every channel built into the router. Shared secrets and failures, a
- release per channel. **Rejected.**
-- **e. A channel module per service, with its own approval or question path** (a "Telegram module"
- that decides things). It locks the conversation to one service, and the next channel repeats it.
- **Rejected.**
-- **f. Kinded benches.** Two mesh seats, **`channel`** (out) and **`intake`** (in). Their holders are
- different modules, each claiming a **kind** (`telegram`, `desktop`, `ntfy`, `matrix`, `mail`, …).
- - One holder per kind; two claiming one kind is refused at registration.
- - A verb's subject carries the kind, as a node seat's carries the machine:
- `mesh.seat.channel.tool.send.`.
- - A new channel is a new module claiming a new kind, with no change to the router.
- - **Chosen.**
-
-Option f needs a second bench, of a new sort (different modules, keyed by kind), which ADR 0223 says
-must be decided. It also needs a claim that carries a kind and capabilities.
-
-## The channel seat (out)
-
-**`channel`**, mesh scope, a kinded bench.
-
-### Served
-
-- **`send`:** words, a priority, whether silent, an optional **ask block** (the kind, the options each
- with an opaque token, the ask id) and an optional thread (the conversation handle, or the message
- this replies to). Answers the channel's id for what it sent, or a refusal in words.
-- **`edit`:** replace a sent message by its id, where `edit` is declared.
-- **`standing`:** ready, not configured (naming what is missing, never a value), or failing (since
- when, why); the last delivery; the declared capabilities.
-
-### Emitted
-
-- **`delivered`** and **`failed`**, the latter marked **permanent** or **transient** (defect D6 in
- [04](04-telegram-as-the-first-holder.md)).
-
-### Honoured
-
-- The declared maximum length, by cutting and saying so (D1).
-- Silence where declared (D4).
-- An identical edit is a success (D6).
-- No own secret in any error or event.
-
-### The content rule
-
-The content rule is the router's, applied before anything reaches a holder that does not declare
-`private`.
-
-## The intake seat (in)
-
-**`intake`**, mesh scope, a kinded bench. A service that is read and written by one program (a
-Telegram bot, [04](04-telegram-as-the-first-holder.md)) is held by one module claiming both seats under
-one kind.
-
-A holder turns what arrives into **one envelope**:
-
-| Field | What it is |
-|---|---|
-| `id` | Unique, for deduplication. |
-| `kind` | The holder's kind. |
-| `what` | `message` (written first), `choice` (a button or a reaction), `reply` (written in an ask's thread), `mail`, `call` (a webhook), `seen` (activity, for the work context). |
-| `sender` | The identity on that service, and whether the service authenticated it. |
-| `operator` | Whether that identity is on the **controller's** list of the operator's identities. Filled from that list, never from the holder's own. |
-| `conversation` | An opaque handle. Sending on it reaches the same chat, room or mail thread. |
-| `in-reply-to` | The ask or message it answers, if any. |
-| `payload` | The text, or the option's token. **No secrets:** a code for an authorising ask never travels in an envelope ([08](08-asks-that-authorise.md)). |
-| `at` | When. |
-
-**Answers go to the ask's owner by request and reply:**
-- the router's `answer` for an ordinary ask;
-- the controller's for an authorising one.
-
-**Everything else is an event on the seat,** which consumers take by `what`:
-- the router takes `seen` for the work context;
-- an agent bridge takes `message` from the operator;
-- a future mail rule takes `mail`.
-
-**One gap to close:** the shared library cannot yet publish on a seat's event subjects (design 32 §1).
-
-## The capability vocabulary
-
-**Fixed and versioned:** `channel-capabilities/1`. A holder declares capabilities in its claim, and
-the catalogue refuses a word outside the vocabulary. **Each capability has a contract test** the
-holder's build runs and a **drill** its `standing` can run. One that fails its drill is reported and
-withdrawn from routing until it passes.
-
-The words are in three groups. The first two serve every conversation. The third exists only for
-asks that authorise, and is defined in [08](08-asks-that-authorise.md).
-
-### Delivering
-
-| Capability | Promise | Test / drill |
-|---|---|---|
-| `deliver` | It arrives, or `failed` says why. | Against a test double; a live test message. |
-| `reaches-away` | It reaches a phone away from the operator's machines. | Declared by kind; the operator acknowledges a drill. |
-| `loud` | It can break through the phone's quiet hours. | The service's override is set for urgent. |
-| `silent` | It can arrive without sound. | The service's silent flag is set. |
-| `edit` | A sent message can be replaced in place. | Edit and read back, against a double. |
-| `max-length:N` | Up to N characters arrive whole; longer is cut, and the cut is said. | N+1 characters give a cut message, not a failure. |
-| `reaches-when-mesh-down` | Delivering needs neither the bus nor the control node. | Checked by the holder's placement and its send path. |
-| `private` | The words stay on the operator's machines, or are end-to-end encrypted. | Declared by kind; reviewed. |
-
-### Conversing
-
-| Capability | Promise | Test / drill |
-|---|---|---|
-| `choice` | The operator can pick one offered option in one act, and the pick comes back. | A simulated tap gives a `choice` envelope with the option's token. |
-| `reply` | The operator can answer in free text, and it comes back. | A simulated reply gives a `reply` envelope. |
-| `threads` | An answer is tied to the message it answers. | A reply to message A carries A in `in-reply-to`. |
-| `operator-first` | The operator can write to the mesh unprompted. | A simulated message gives a `message` envelope. |
-
-### Trusting
-
-`verified-sender`, `exact-render`, `code-factor` and `key-factor` are defined in
-[08](08-asks-that-authorise.md). A channel without them still converses fully. It just cannot carry
-an answer that performs an action.
-
-## Agents in the conversation
-
-An agent is a participant. It asks, and it receives.
-
-- **An agent whose operator is at its own terminal** asks there, in the terminal. The seat is not
- needed.
-- **An agent working unattended** (in the background, on a schedule, or with the operator stepped
- away) asks **through the seat**. The router puts the ask where the operator is now
- ([07](07-the-work-context-and-the-desk.md)): the desk if they are at it, Telegram if they are away
- or talking through Telegram. The agent waits for, or polls, the answer.
-- **An agent the operator talks to through Telegram** receives the operator's messages as intake
- envelopes. It answers on the conversation handle. Its asks carry that handle, so they appear in the
- same chat.
-- **An agent's words reach the operator only as an asker's words,** and the operator's answer reaches
- the agent only as the operator's. An agent relaying "the operator said yes" is not an answer to
- anything. That is why an answer comes from a channel holder, never from the asker
- ([08](08-asks-that-authorise.md)).
-
-## The watcher, in this shape
-
-The watcher's watcher stays **outside** the seats, deliberately:
-- it must speak when the bus and the control node are what failed;
-- the seats live on the bus.
-
-It is a minimal `deliver` + `reaches-when-mesh-down` sender with its own bot. It never reads, and never
-asks. Its sibling outside the mesh is the dead-man service ([05](05-the-other-holders-on-the-same-axes.md)).
-
-## From today to this shape
-
-1. **Fix the first holder in place:** D1–D4 of [04](04-telegram-as-the-first-holder.md). No change of
- shape.
-2. **The operator configures Telegram** ([09](09-a-proposed-decision.md)). The mesh starts telling.
-3. **The vocabulary and the kinded bench in the catalogue,** and seat events in the shared library.
-4. **Split Telegram out** into its own module holding `channel` and `intake` under `telegram`. The
- router keeps the desktop adapter as `channel/desktop` and `intake/desktop`.
-5. **Asks in the router,** with buttons and replies on Telegram and actions on the desktop
- ([07](07-the-work-context-and-the-desk.md)). Agents can ask from here on.
-6. **The work context** as the router's ordering ([07](07-the-work-context-and-the-desk.md)).
-7. **Authorising asks in the controller** ([08](08-asks-that-authorise.md)).
-8. **Operator-first messages,** and an agent bridge consuming them.
-9. **Further holders as wanted:**
- - Pushover for waking;
- - Matrix once its push is measured;
- - mail out for the digest, mail in as an intake.
-
-The watcher changes only at step 1.
-
-## What this revisits in [03](03-open-questions.md)
-
-- **Q1:** channels attach as holders of kinded benches (f), not as contributions.
-- **Q3:** the life of a message gains the life of an ask. `edit` and `silent` are declared, and decide
- how a clearing is said (D2, D3).
-- **Q4:** presence becomes the work context ([07](07-the-work-context-and-the-desk.md)).
-- **Q7:** answering back is the conversation itself. Its authorising layer is
- [08](08-asks-that-authorise.md).
-- **Q8:** the content rule stays, applied by the router. Whether a `private` holder may be exempted
- is left to graduation.
diff --git a/01-RESEARCH/028-the-meshs-output-channel/07-the-work-context-and-the-desk.md b/01-RESEARCH/028-the-meshs-output-channel/07-the-work-context-and-the-desk.md
deleted file mode 100644
index c46219fd..00000000
--- a/01-RESEARCH/028-the-meshs-output-channel/07-the-work-context-and-the-desk.md
+++ /dev/null
@@ -1,117 +0,0 @@
-# 07 — The work context, and the desk
-
-The operator, 2026-10-06:
-
-- "If dunst can also show buttons, we could prefer to use desktop notifications instead of Telegram
- for approval actions when working in a session."
-- "The work context is an important factor when deciding the correct output channel."
-
-[06](06-a-conversation-with-the-operator.md) decides **which channels may carry** a message or an ask:
-those whose declared capabilities satisfy it. This document decides **which of those comes first**,
-from where the operator is working. It also makes the desk a full participant in the conversation.
-
-## The rule
-
-> **A message or ask goes to the most direct channel in the operator's current context, among those
-> whose capabilities already satisfy it. Unanswered in time, it escalates along a fixed chain.
-> Context orders the candidates; it never adds one. Context never lowers the bar.**
-
-The last sentence matters most for asks that authorise ([08](08-asks-that-authorise.md)). Being at the
-desk never makes a click count as more than it proves.
-
-## The signals
-
-| Signal | Source | Read as |
-|---|---|---|
-| A graphical session unlocked, with input in the last 5 minutes, on machine M | The `node-lock-screen` seat's holder on M (the screen-lock module), whose tools already read the idle time and the lock state. Underneath: logind's `LockedHint` and `IdleSinceHint`. Proposed: an event on each change, not a poll. | **at the desk on M** |
-| That session locked, or idle longer | the same | **not at the desk** |
-| An agent asking from machine M | The ask's asker names its machine. An agent module's own "session active" event, when one exists. | **working with an agent on M**. It strengthens "at the desk on M"; alone it proves nothing. |
-| A verified intake `message`, `reply` or `choice` in the last 15 minutes | The intake seat ([06](06-a-conversation-with-the-operator.md)) | **in a conversation** on that kind |
-| An ask carrying a conversation handle | The ask | **that conversation**, whatever else is true |
-| The hour, against quiet hours | The router's setting | **night**: only urgent wakes |
-
-## The contexts, and where things go
-
-"The away channel" is the operator's setting (Telegram, to begin with). "The loud holder" is an
-optional second away holder for waking (Pushover, [05](05-the-other-holders-on-the-same-axes.md)).
-
-| Context | An ask goes to | Urgent message | Warning | Unanswered or unacknowledged → |
-|---|---|---|---|---|
-| **In a conversation through Telegram** (the ask carries its handle, or Telegram activity is newer than any desk input) | that chat, in the thread | that chat | that chat, silent | after 10 min (urgent) or 1 h: also the desk, if active |
-| **At the desk on M** (with or without an agent there) | the desk on M, if it can carry the ask; otherwise the away channel, and the desk says where it went | the desk on M, and the away channel silently | the desk on M | after 5 min (urgent) or 30 min: the away channel, with sound |
-| **Away** (no unlocked active session, no recent conversation) | the away channel | the away channel | the away channel, silent | after 15 min (urgent): the loud holder, if configured |
-| **Night, away** | non-urgent asks wait for the morning; urgent as away | the away channel and the loud holder | the morning digest | as away |
-| **The desk locks while an ask is shown there** | moves at once to the away channel | — | — | — |
-
-- **Every copy of an ask stays valid until one answer wins.** The others are edited to say where it
- was answered.
-- **The context's channel cannot carry the ask** (a free-text ask at a desk without a prompt, or an
- authorising ask the desk cannot prove): the next in the chain carries it, and the context's channel
- says where it went.
-- **Nothing can carry it:** the router says so, as a condition of its own.
-
-### Presence stays in the mesh
-
-- **Presence facts are events on the bus,** consumed by the router.
-- **They are kept as current state only:** a key-value entry per machine and per intake kind,
- overwritten, never a history.
-- **They never appear in a message's words,** so they never reach a channel that is not `private`.
-- **No module keeps them as a timeline of the operator's day.** A consumer that wants one is a
- decision of its own.
-
-## The desk as a participant
-
-Read from the catalogue's main branch and the tools' current documentation, 2026-10-06.
-
-### What exists
-
-- **The `node-notifier` seat** is held on each graphical machine by the dunst module. Its `send` runs
- `notify-send --print-id` with an urgency, an application name and an optional replace id. It
- carries **no actions** today.
-- **libnotify's `notify-send`** (0.8 and later) takes `--action=NAME=Label`, repeatable, and `--wait`.
- It prints the chosen action's name when one is chosen, and nothing when the notification is closed.
- Underneath, the notification server emits `ActionInvoked` with the notification's id and the
- action's key.
-- **dunst shows actions:**
- - `do_action`, which the module binds to the **middle** click, invokes the default or only action;
- - otherwise it opens the **context menu**, which in this mesh is the `node-launcher` seat's
- dmenu-compatible menu;
- - `dunstctl action` and `dunstctl context` do the same from a command line.
-- **The `node-launcher` seat's `menu` verb** shows a list in the operator's session and answers the
- chosen line. A dmenu-compatible menu also accepts typed text that is not a listed line, which makes
- it a free-text prompt.
-- **The graphical session is X11.**
-
-### What it takes
-
-- **`send` gains actions:** a list of (token, label).
-- **It still answers at once.** A notification may be answered minutes later.
-- **The holder listens for `ActionInvoked`** and emits the chosen token as an event on its node seat.
-- **The router's desktop adapter,** which holds `channel/desktop` and `intake/desktop`, turns that
- into a `choice` envelope.
-- **For `text`, `number` and `date` asks,** the notification's single action opens the launcher's
- prompt, and what is typed comes back as a `reply`.
-
-### What the desk can declare
-
-| Group | Capabilities |
-|---|---|
-| Delivering | `deliver`, `silent` (low urgency), `loud` (critical urgency stays until dismissed), `edit` (replace id), `private` |
-| Conversing | `choice` (actions), `reply` (through the launcher's prompt), `threads` (the ask's id is carried) |
-| Trusting | **not** `verified-sender`. `exact-render` yes. `code-factor` (a prompt) yes. `key-factor` yes where a security key is plugged in. See [08](08-asks-that-authorise.md). |
-
-So the desk carries **every ordinary ask**: yes or no, one of, text, number, date and acknowledge.
-It needs no account anywhere. An agent working unattended on the workstation asks a clarifying question,
-and the operator, at the desk, answers it in the notification.
-
-It carries an **authorising** ask only with a factor the controller verifies itself, because a click
-on an X11 desk proves that someone was there, not that the operator clicked
-([08](08-asks-that-authorise.md)).
-
-## Sources
-
-- `notify-send(1)`, `--action` and `--wait`: https://man.archlinux.org/man/notify-send.1.en
-- Desktop notifications and actions: https://wiki.archlinux.org/title/Desktop_notifications
-- dunst documentation (mouse actions, `do_action`, the context menu): https://dunst-project.org/documentation/
-- logind's `LockedHint`, `IdleHint`, `IdleSinceHint`:
- https://freedesktop.org/software/systemd/man/org.freedesktop.login1.html
diff --git a/01-RESEARCH/028-the-meshs-output-channel/08-asks-that-authorise.md b/01-RESEARCH/028-the-meshs-output-channel/08-asks-that-authorise.md
deleted file mode 100644
index ca995799..00000000
--- a/01-RESEARCH/028-the-meshs-output-channel/08-asks-that-authorise.md
+++ /dev/null
@@ -1,247 +0,0 @@
-# 08 — Asks that authorise
-
-Most asks inform their asker and change nothing ([06](06-a-conversation-with-the-operator.md)). Some
-answers **perform an action**: approving a retirement, confirming that a binding moves, deleting data.
-This document is the layer those asks need on top of the conversation. It is the controller's checks,
-the trust a channel must prove, and what a compromise can reach.
-
-The operator, 2026-10-06:
-
-- "Make sure I can approve and reject stuff via the Telegram channel."
-- In a terminal session with an agent, having to open Telegram is acceptable.
-- But when talking to an agent **through** Telegram, the operator cannot switch to a desktop session.
-- At the desk, desktop buttons would be preferred ([07](07-the-work-context-and-the-desk.md)).
-
-## Where this stands against what was decided
-
-- To-be 45 §5 says: "No answering back in this form".
-- ADR 0227 kept it open on purpose: "routing by presence, quiet hours, **answering back** and the
- external dead-man service stay open in 028, whose graduation amends to-be 45."
-- So this answers 028's Q7. It does not reverse ADR 0227.
-
-## What there is to authorise
-
-Read from the controller's main branch on 2026-10-06. The condition store marks conditions only a
-person resolves (`resolver: operator`): from the start for retirement, clean-up and binding conditions,
-and once a healer's budget is spent.
-
-| Action | The verb today | Asked for by | Reversible | Tier |
-|---|---|---|---|---|
-| Approve the retirement set waiting | `retire approve --why` | `retire-waiting` (urgent) | yes: asking for a consumer again re-enables it | approve |
-| Reject it | `retire reject … --why` | `retire-waiting` | yes | approve |
-| Approve a set rejected before | `retire approve …` | `retire-rejected` (warning) | yes | approve |
-| Confirm that a binding moves, once its data is moved | `pin ` | `binding-kept` (urgent) | the pin, yes. The data, not by the mesh. | approve |
-| End a stuck plan | `plans stop` / `close --why` | `stalled`, `sent-not-reported` once escalated | no, but it destroys nothing | approve |
-| Send a machine its declaration by hand | `push --why` | `sent-not-reported` once escalated | n/a | approve |
-| Reset a bus consumer's position | `broker consumer-reset --why` | `consumer-behind` once escalated | no: messages skipped or redelivered | approve (graduation to confirm) |
-| Try a healer's repair once more | the healer's ordinary path | any escalation (H1–H5), `healers-braked` | as the repair is | approve |
-| Silence a condition | `conditions silence --for --why` | any | yes, it ends by itself (at most 7 days) | acknowledge |
-| Delete one retired consumer's data | `cleanup delete --why` | `cleanup-waiting` (after 30 days) | **no** | destroy |
-| Delete everything retired longer than N days | `cleanup delete --older-than N --confirm --why` | `cleanup-waiting` | **no** | destroy |
-| Change the operator's identities, the away channel, or a factor's enrolment | (new) | — | yes, but it changes who may authorise | destroy |
-
-The build queue verbs and `replay --register` are not offered as asks: no condition asks for them.
-
-## Trust, as capabilities and proofs
-
-The conversation's vocabulary ([06](06-a-conversation-with-the-operator.md)) gains four words used
-only here:
-
-| Capability | Promise | Test / drill |
-|---|---|---|
-| `verified-sender` | The holder proves the answer came from the operator's own account on that service, by the service's authentication, through a holder **no agent shares**: not on the operator's account, not on a machine where agents run as the operator. | A choice from an identity not on the list is dropped and reported. The holder's placement is checked. |
-| `exact-render` | The ask is shown as the controller rendered it, by the holder itself. No asker or agent composes the words the operator authorises. | Rendered text equals the controller's, byte for byte, against a double. |
-| `code-factor` | The holder can carry a code the operator types to the controller, by request and reply, and never judges it. | A code is never in an event, and is deleted from the conversation where the service allows. |
-| `key-factor` | The holder can run a security key's assertion over the controller's challenge, and hand the controller the result. | The challenge is the controller's, and the signature is checked by the controller. |
-
-From these, three **proofs** that the operator is the one answering:
-
-- **P1, a verified sender:** a Telegram tap, through a holder on a machine no agent runs on.
-- **P2, a code:** from the operator's authenticator, verified by the controller.
-- **P3, a key touch bound to the ask:** the controller's challenge is a hash of the ask's id and the
- state digest. A FIDO2 assertion with user presence (the key waits for a touch) is checked against the
- operator's enrolled credential. It proves a physical touch for **this** ask and no other.
-
-## Three tiers
-
-| Tier | Required |
-|---|---|
-| **acknowledge** | `choice`, `exact-render`. Silencing is open to agents already, and announced; a proof adds nothing. |
-| **approve** | `choice`, `exact-render`, and **one** proof (P1, P2 or P3). Single use, bound to the exact state shown, expiring when that state changes or after 24 h. |
-| **destroy** | `choice`, `exact-render`, and **two** proofs, at least one of them P2 or P3. Valid 10 minutes after it is shown; at most one destroy answered per 10 minutes. |
-
-| Where the operator answers | Proofs it offers | acknowledge | approve | destroy |
-|---|---|---|---|---|
-| Telegram | P1 (tap), P2 (code as a reply) | tap | tap | tap and code |
-| the desk, with a security key | P3 (touch), P2 (code in a prompt) | click | click and touch | click, touch and code |
-| the desk, without a key | P2 (code in a prompt) | click | click and code | not possible: carried by the away channel |
-| an agent's terminal | none | none | none | none: an agent asks, it never answers |
-| the console (a shell) | P2 (code) | — | break-glass: a code | none |
-
-### The rule
-
-1. **Every authorising verb declares its tier** in the controller's verb table.
-2. **An authorising ask is offered only on a channel whose capabilities satisfy its tier.** An answer
- arriving from any other channel is refused, and the refusal is said there.
-3. **The operator's away channel must satisfy every tier.** The self-check verifies it. A setting
- that would make a tier possible only at a desk is refused, unless the operator chose that for the
- tier explicitly. While working through Telegram, everything can be completed in Telegram.
-4. **The work context chooses among the channels that qualify; it never makes one qualify**
- ([07](07-the-work-context-and-the-desk.md)).
-5. **No agent authorises.** An agent asks. It holds no verb that performs an authorising action. The
- record names the agent that asked.
-
-## Why the desk needs a factor
-
-- **X11 does not isolate the clients of one display.** Any of them can inject input (the XTEST
- extension, as `xdotool` does) and read keystrokes.
-- **`dunstctl action` invokes a notification's action** for any program of the account.
-- **The desktop holder runs as the operator's account,** whose files, the holder's bus credential
- included, every agent on that account can read.
-
-So a click at the desk, its `ActionInvoked` and the desktop holder's envelope can all be produced by
-an agent. An unlocked session with recent input proves a person was there, not that the person
-clicked. The desk declares no `verified-sender`. A factor the **controller** verifies gets around
-that.
-
-| Factor at the desk | Can an agent on the account fake it? | Judgement |
-|---|---|---|
-| **A security key's touch, bound to the ask** (P3) | No: the touch is physical, and the signature covers this ask's id and state. | **Preferred.** One click, one touch, no phone. Needs a key and an enrolment. |
-| **A code typed into the launcher's prompt** (P2) | It cannot know the code. On X11 it can read keystrokes and race to use the code first, and each step's code is accepted once. | **Acceptable** without a key. Costs picking up the phone. The race is a residual risk until the session leaves X11. |
-| **The screen's unlock or a fingerprint** | Yes: only the local holder sees the result. | **Rejected.** |
-
-The desk would earn `verified-sender` only if agents ran under an account of their own, without the
-operator's display, session bus or holders' credentials, on a compositor that isolates clients. That
-is a question for the agent modules' placement. It is noted, not proposed.
-
-## The controller holds authorising asks
-
-Today a grant covers a whole verb (`seat:mesh-controller.retire` allows approving and listing alike).
-A hand-act records `by` from the calling bus principal, which for a channel would be the module, not
-the person. Both call for the controller to hold these asks itself.
-
-- **The verb table gains a field.** The controller's verb definition (a name, a description, input and
- output schemas) gains **`authorises`**: the tier, and the arguments that make up the exact state a
- person must see (for `retire approve`, the set of consumers).
-
-### Three verbs
-
-- **`authorise request`:** anyone may call it, an agent or the router on a condition's behalf. It
- carries the action (verb and exact arguments), why, and optionally a conversation handle.
- - The controller renders the ask: what is asked, the exact state, who asks, what each option does.
- - It stores it with an opaque id (10 random base32 characters), its expiry and a digest of the
- state shown, and emits `ask-opened` with the controller as owner.
- - The router carries it like any ask. **Nothing is performed.**
-- **`authorise answer`:** only intake holders are granted it. It carries the id, the option, the
- sender's identity, and the code or key assertion where the tier needs them. The controller checks,
- and refuses at the first failure:
- 1. the ask is open and not expired;
- 2. the caller is the holder of an intake kind, by the controller's own seat records, never by the
- request's claim;
- 3. that kind's declared capabilities, from the controller's records, satisfy the tier, and the
- proofs present are enough;
- 4. a P1 answer: the sender is on the controller's list of the operator's identities for that kind;
- 5. a code: valid for the current or previous 30-second step, and unused;
- 6. a key assertion: it verifies against the enrolled credential, over this ask's challenge, with the
- user-presence bit set;
- 7. the state **now** has the digest it had when shown. Otherwise the ask is void and a new one is
- requested.
-
- Then it performs the action as itself, records the hand-act, closes the ask with a compare-and-set
- (so a second answer on another channel loses), and emits `ask-answered`.
-- **`authorisations`:** open and recent authorising asks.
-
-### The authorising verbs refuse to be called directly
-
-`retire approve|reject`, `cleanup delete`, and `pin` while a `binding-kept` names it refuse a caller
-unless the call comes through `authorise answer`, or carries a valid code as **break-glass** at the
-console. Break-glass is recorded as such, and announced on every channel.
-
-- `retire approve` must accept the set it approves (`expect`). Today it re-reads the set when it
- runs, so "approve what you were shown" (ADR 0230) does not hold end to end.
-- `conditions silence` stays callable. A silence an agent sets is said on the away channel, with its
- why.
-
-### The record
-
-The hand-act gains:
-- **`via`:** the kind and the holder;
-- **`requested-by`:** the agent principal or the condition key;
-- **`ask`:** the id;
-- **`proofs`:** which of P1, P2 and P3 were present.
-
-`by` reads "the operator, as identity ". Every copy of the ask is edited to the outcome
-("approved by the operator on telegram at 14:02 UTC"), and its buttons are removed.
-
-## Telegram, carrying them
-
-- **Buttons.** `callback_data` is at most 64 bytes, so a button carries only `a1::`.
- Everything else is in the controller.
-- **A tap** arrives as a `callback_query` with the tapping user's id and the chat. The holder:
- 1. drops it, and reports, unless both are the operator's;
- 2. calls `answerCallbackQuery` at once, because the phone shows a spinner until it does;
- 3. calls `authorise answer`;
- 4. edits the message to the outcome or the refusal.
-- **A destroy ask** answers the tap with a `ForceReply` prompt for the code. The holder reads the
- operator's reply, deletes it from the chat (bots may delete incoming messages in private chats), and
- hands it to the controller by request and reply. It is never put in an event.
-- **Long polling, not a webhook.**
- - `getUpdates` needs no route into the mesh.
- - A stolen token can steal updates, and a second reader shows as HTTP 409, but it cannot inject an
- update.
- - A webhook needs a public route, and a stolen token can redirect it.
- - The offset is kept in the holder's state, and ids are single use anyway.
-- **Placement.** The Telegram holder runs where no agent runs as the operator. Its own declaration of
- `verified-sender` depends on it.
-
-## Agents
-
-- **In a terminal:**
- 1. The agent calls `authorise request`.
- 2. The router carries the ask to where the operator is ([07](07-the-work-context-and-the-desk.md)):
- the desk, with a factor, or Telegram.
- 3. The agent sees `ask-answered`.
-
- Nothing the agent says counts.
-- **Through Telegram:** the request carries the conversation handle, so the ask appears in the same
- chat, rendered by the holder, and the operator taps in place, destroy included.
-
-## If something is compromised
-
-| What is lost | What the attacker can do | What limits it |
-|---|---|---|
-| The bot token | Read what the bot is sent from then on. Steal taps (visible as 409). Send the operator fake messages. | It cannot call `authorise answer`. Revoke with BotFather's `/token`. |
-| The operator's Telegram account, on a new device | Approve or acknowledge. | Telegram's two-step password. Every authorisation is announced on the other channels. Approve is reversible. Destroy needs a code. |
-| The phone, unlocked | Everything, including destroy, if the authenticator is open on it. | An authenticator behind biometrics. One destroy per 10 minutes, announced. Backups (research 030). A setting turning destroy off for the away channel. |
-| An agent on the operator's account | Click at the desk, read the desk's keystrokes, read the desktop holder's credential. | No tier accepts the desk without a code or a key touch the controller verifies. A code read off X11 is good for one step, and the race is said above. |
-| A channel module, or its bus account | Forge P1 for approve or acknowledge. | It cannot forge P2 or P3. Destroy needs one of them. |
-| Telegram itself | Read the words. In principle, forge a tap. | The content rule. Destroy needs P2 or P3, which Telegram never sees. |
-
-**The factors' secrets are the controller's own.**
-- The TOTP seed is made by the mesh. It is enrolled by showing its URI once, only to a terminal, and
- never through a channel or an event.
-- A security key is enrolled by registering its credential's public key.
-- Re-enrolling either is a destroy ask.
-
-## The other holders, for authorising
-
-- **Matrix** can declare everything Telegram does: reactions, replies, a sender authenticated by the
- mesh's own homeserver, codes. It is the self-hosted carrier once its push is measured.
-- **ntfy:** an `http` action makes the phone call the mesh, with a credential inside the notification.
- Nobody knows who tapped, and there is no reply. No tier.
-- **Pushover:** acknowledgement, read back by polling a receipt. At most `acknowledge`.
-- **Mail:** a reply can carry a code, but the sender is forgeable. No tier, except as break-glass.
-
-## Sources
-
-As in [04](04-telegram-as-the-first-holder.md) and [07](07-the-work-context-and-the-desk.md), and:
-- Bot API `callback_data` (1–64 bytes), `answerCallbackQuery`, `ForceReply`, `deleteMessage`:
- https://core.telegram.org/bots/api
-- `answerCallbackQuery` is required even with no text: https://gramio.dev/telegram/methods/answercallbackquery
-- X11 and its clients (input injection, keystroke reading):
- https://hackindex.io/services/x11/exploitation/x11-session-hijacking and
- https://www.semicomplete.com/projects/xdotool/
-- `fido2-assert` (user presence, verifying an assertion):
- https://developers.yubico.com/libfido2/Manuals/fido2-assert.html
-- ntfy `http` actions: https://docs.ntfy.sh/publish/
-- Pushover receipts: https://pushover.net/api
diff --git a/01-RESEARCH/028-the-meshs-output-channel/09-a-proposed-decision.md b/01-RESEARCH/028-the-meshs-output-channel/09-a-proposed-decision.md
deleted file mode 100644
index bfc8f5e2..00000000
--- a/01-RESEARCH/028-the-meshs-output-channel/09-a-proposed-decision.md
+++ /dev/null
@@ -1,238 +0,0 @@
-# 09 — A proposed decision, and what the operator does now
-
-This is the effort's reading as of 2026-10-06, written so that playbook 02 can turn it into a record
-and amend to-be 45 §5. It is a proposal: nothing here is decided until it graduates.
-
-## The recommendation, short
-
-1. **The mesh holds a conversation with the operator over channels.** It sends messages and asks;
- the operator answers or writes first. A message, an ask and an operator message are the three
- things said ([06](06-a-conversation-with-the-operator.md)).
-2. **Channels and intake are seats.** One kinded bench each, `channel` and `intake`. Each holder is a
- module of its own, claiming a kind and declaring capabilities from a fixed, versioned vocabulary,
- each with a contract test and a drill.
-3. **Asks are general.** Yes or no, one of, text, number, date, acknowledge, each requiring its own
- capabilities. They have timeouts, defaults, cancellation, batching and history. The answer returns
- to the asker as an event, and an asker may wait.
-4. **The output seat's holder is the router.** It chooses among the channels that satisfy a message or
- ask, by **work context**: in a Telegram conversation, Telegram; at the desk, the desk; away, the
- away channel. Unanswered, it escalates. **Context never lowers the bar** ([07](07-the-work-context-and-the-desk.md)).
-5. **The desk is a full participant.** Notification actions and the launcher's prompt carry every
- ordinary ask, with no account anywhere.
-6. **Asks that authorise are a layer on top,** held by the controller. Three tiers (acknowledge,
- approve, destroy), and three proofs the operator is answering (a verified Telegram sender, a code,
- a security key's touch bound to the ask). The controller checks the channel's declared
- capabilities from its own records and verifies codes and key assertions itself
- ([08](08-asks-that-authorise.md)).
-7. **No agent authorises.** An agent asks; the operator answers where they are. On an X11 desk, where
- an agent could click for them, only a code or a key touch counts. In a Telegram conversation, the
- ask appears in that chat and is completed there, destroy included.
-8. **Telegram is the first holder and the away channel.** It is free, on both phone platforms, needs
- no server of the mesh's own, and is the only candidate that carries every tier
- ([04](04-telegram-as-the-first-holder.md), [05](05-the-other-holders-on-the-same-axes.md)).
-9. **The watcher's watcher stays outside the seats,** with its own bot, on a machine that is not the
- control node. A free outside dead-man service covers the rest. Pushover is the optional holder for
- waking. Matrix is the self-hosted carrier once its push is measured.
-10. **The built Telegram code needs D1–D4 fixed before it is configured**
- ([04](04-telegram-as-the-first-holder.md)).
-
-## What the operator does
-
-Minimal, in order. Steps 1–6 are possible today. The desk needs nothing from the operator: no account,
-no bot.
-
-1. **Make two bots.** In Telegram, open BotFather and run `/newbot` twice: one for the mesh's
- conversation, one for the watcher. Keep each token out of agent sessions.
-2. **Close them to groups:** `/setjoingroups` → Disable, for each bot.
-3. **Press Start** in each bot's chat. A bot cannot write first.
-4. **Turn on Telegram's two-step verification,** if it is not on.
-5. **Find your chat id.** Until the linking verb exists, call `getUpdates` once, reading the token from
- a file, and take `message.chat.id` from your `/start`. It is the same for both bots.
-6. **Give the mesh the values,** through the controller, never on disk:
- - accept the mesh bot's token as the output seat's holder's own secret `telegram-token`, and set
- its `telegram-chat-id`;
- - assign the watcher to a machine that is not the control node, accept the watcher bot's token,
- and set its chat id;
- - push both machines, and run each module's test verb.
-7. **Later, once built:**
- - make a free dead-man check, and give its ping address to the mesh;
- - enrol an authenticator, at a plain terminal;
- - optionally, enrol a security key, to approve at the desk with one touch.
-
-## What would change in the code (proposal, not built)
-
-- **Now, in the output seat's holder and the watcher:** D1 (cut at 4096 characters), D2 (a reopening
- is a new message), D3 (a clearing reaches the newest message), D4 (`disable_notification` for
- warnings and clearings), then D5–D10.
-- **Catalogue:**
- - a claim carries `kind` and `capabilities`;
- - a kinded bench refuses two holders of one kind;
- - the vocabulary `channel-capabilities/1` and its contract tests;
- - the shared library publishes on a seat's event subjects.
-- **Router:**
- - asks (`ask`, `ask cancel`, `asks`, `answer`, and the events `ask-opened`, `ask-answered`,
- `ask-closed`);
- - the work context, from presence events;
- - the escalation chain.
-- **Desktop:** `node-notifier.send` gains actions, and the holder emits the chosen one. The
- screen-lock holder emits lock and idle changes.
-- **Controller:**
- - `authorises` on the verb definition;
- - `authorise request`, `authorise answer`, `authorisations`;
- - `retire approve` takes `expect`;
- - the authorising verbs refuse direct calls without a code;
- - the hand-act gains `via`, `requested-by`, `ask` and `proofs`;
- - the operator's identities, the TOTP seed and enrolled keys as its own;
- - a self-check probe: the away channel satisfies every tier.
-- **Modules:**
- - a `telegram` module holding `channel` and `intake` (long polling, buttons, replies, `ForceReply`
- codes, a linking verb with a one-time deep-link code), placed where no agent runs as the operator;
- - an agent bridge for operator messages;
- - the dead-man ping.
-
-## The tables
-
-### Ask kinds × what a channel needs
-
-| Ask | deliver | choice | reply | threads | Trust (only if it authorises) |
-|---|---|---|---|---|---|
-| a message (no answer) | ✓ | | | | |
-| acknowledge | ✓ | ✓ | | | tier acknowledge: `exact-render` |
-| yes-no | ✓ | ✓ or | ✓ | ✓ | tier approve or destroy, if it authorises |
-| one-of | ✓ | ✓ or | ✓ (a number) | ✓ | as above |
-| text, number, date | ✓ | | ✓ | ✓ | never authorises |
-
-### Authorising tiers × proofs
-
-| Tier | Needs | Telegram | Desk with a key | Desk without a key |
-|---|---|---|---|---|
-| acknowledge | `choice`, `exact-render` | tap | click | click |
-| approve | + one proof | tap (P1) | click + touch (P3) | click + code (P2) |
-| destroy | + two proofs, one of them P2 or P3 | tap + code (P1 + P2) | click + touch + code (P3 + P2) | carried by Telegram |
-
-### Surfaces × declared capabilities
-
-✓ declared, ~ conditional, blank not.
-
-| Surface | deliver | reaches-away | loud | silent | edit | choice | reply | threads | operator-first | verified-sender | exact-render | code-factor | key-factor | private | reaches-when-mesh-down |
-|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
-| telegram | ✓ | ✓ | | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ (placed apart from agents) | ✓ | ✓ | | | |
-| desktop (dunst + launcher) | ✓ | | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | | ✓ | ✓ | ~ (a key plugged in) | ✓ | |
-| matrix (own server) | ✓ | ~ (push unmeasured) | | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | ~ | |
-| ntfy | ✓ | ✓ | ✓ | ✓ | | ~ (http action) | | | | | ✓ | | | ~ | ~ (ntfy.sh) |
-| pushover | ✓ | ✓ | ✓ | ✓ | | ~ (acknowledge) | | | | ✓ | ✓ | | | | ✓ |
-| mail (outside relay) | ✓ | ✓ | | ✓ | | | ✓ | ✓ | ✓ | | ✓ | ~ | | | ✓ |
-| an agent's terminal | — | | | | | | ~ (relayed) | | | | | | | | |
-| the console | — | | | | | | | | | | ✓ (own output) | ✓ | | | |
-| the watcher's sender | ✓ | ✓ | | | | | | | | | | | | | ✓ |
-
-### Where things go, by context
-
-The full table is in [07](07-the-work-context-and-the-desk.md). In one line each:
-
-- **in a Telegram conversation:** that chat;
-- **at the desk:** the desk, or the away channel when the desk cannot carry it;
-- **away:** the away channel;
-- **night:** urgent only;
-- **unanswered:** the next in the chain;
-- **the desk locks:** it moves away.
-
-## The proposed record
-
-> **Title.** The mesh holds a conversation with its operator over channels that are seats, chosen by
-> capability and work context, and an answer that performs an action is authorised by the controller.
->
-> **Context.** The output channel was built in its minimal form (ADR 0227, to-be 45 §5): one holder
-> carrying the router, a Telegram client and a desktop adapter, and no answering back.
-> - The operator expects many channels and many inputs.
-> - The operator wants agents and modules to ask questions, not only for permission.
-> - The operator wants the work context to choose the channel, and every authorisation completable on
-> the away channel.
-> - Today, actions that need a person are verbs any granted principal can call, agents included, and
-> a hand-act records the calling principal, not the person.
->
-> **Considered options.**
-> 1. Channels as contributions to the output seat. A channel is running code with a secret and
-> answers, not content a holder places.
-> 2. One seat per channel kind. The router learns every seat.
-> 3. Channel modules found by a manifest field. Callers use seats, never modules (ADR 0126).
-> 4. One notifier with every channel built in. Shared failure, shared secrets, a release per channel.
-> 5. A per-service module with its own approval or question path. Locks the conversation to one
-> service.
-> 6. **Kinded benches for out and in, a capability vocabulary, a router that holds the conversation
-> and orders channels by work context, and an authorising layer held by the controller. Chosen.**
->
-> For answers:
-> - a webhook, or **long polling (chosen)**;
-> - authorising actions called directly by the channel module, or **requested and answered through
-> the controller (chosen)**;
-> - trusting a desktop click, or **requiring a code or a key touch the controller verifies (chosen)**.
->
-> **Decision.**
-> - **The conversation.**
-> - Two mesh seats are kinded benches: `channel` (send, edit, standing) and `intake` (one envelope
-> per input).
-> - Each holder is its own module, claims one kind, and declares capabilities from
-> `channel-capabilities/1`, each with a contract test and a drill.
-> - The output seat's holder routes messages and asks (yes-no, one-of, text, number, date,
-> acknowledge) by required capability, then by work context, then by severity, escalating when
-> unanswered. It says when nothing can carry something.
-> - Asks have timeouts, defaults (never for an authorising ask), cancellation, a per-asker limit,
-> batching and 30 days of history. Answers return to the asker as events.
-> - Presence is current state on the bus, never a history, never in a message's words.
-> - **Asks that authorise.**
-> - The controller holds them: `authorise request` (anyone; never performs), `authorise answer`
-> (intake holders only), `authorisations`.
-> - It checks the caller's declared capabilities from its own records, the sender against its own
-> list of the operator's identities, codes and key assertions itself, and the exact state's digest,
-> single use and expiry.
-> - Tiers acknowledge, approve and destroy require none, one and two proofs. The away channel must
-> satisfy every tier, checked by the self-check, unless the operator chose otherwise for a tier.
-> - No agent authorises. The authorising verbs refuse direct calls except as break-glass with a
-> code.
-> - The hand-act records `via`, `requested-by`, `ask` and `proofs`.
-> - **First holders.**
-> - Telegram is the first holder of both seats and the away channel, placed where no agent runs as
-> the operator.
-> - The desktop holds both for the desk.
-> - The watcher's watcher stays outside the seats with its own bot, and an outside dead-man service
-> is pinged by the self-check and the watcher.
->
-> **Consequences.**
-> - The output seat's holder loses its Telegram client to a module of its own and gains asks and the
-> work context.
-> - The catalogue gains `kind` and `capabilities` on a claim, and a second kind of bench.
-> - `node-notifier.send` gains actions.
-> - The controller's verb definition gains `authorises`, and `retire approve` takes the set it
-> approves.
-> - Agents' grants lose authorising verbs.
-> - To-be 45 §5 is amended: the operator answers.
-> - Telegram sees the words, held to the content rule, and never a factor's secret.
->
-> **How it is checked.**
-> - **Catalogue tests:**
-> - an unknown capability is refused;
-> - two holders of one kind are refused;
-> - each declared capability's contract test runs in its holder's build.
-> - **Router tests:**
-> - an ask goes only to channels whose capabilities satisfy it;
-> - context reorders but never adds a channel;
-> - an authorising ask never defaults;
-> - a cancelled ask's copies are edited;
-> - a fourth open ask from one asker is refused.
-> - **Controller tests,** one per refusal of `authorise answer`:
-> - from a non-intake principal;
-> - from a kind that does not meet the tier;
-> - from an identity not on the list;
-> - with too few proofs;
-> - with a used code;
-> - with a key assertion over another ask's challenge;
-> - with a stale digest;
-> - a second answer.
->
-> Also: a direct `retire approve` without a code.
-> - **Self-check probe:** the away channel meets every tier.
-> - **Live drills:**
-> - an agent's question answered at the desk;
-> - the same with the desk locked, answered on the phone;
-> - an approve and a destroy on a test condition, answered on the phone, with the hand-acts read.
diff --git a/01-RESEARCH/029-the-agent-configured-through-its-module/00-overview.md b/01-RESEARCH/029-the-agent-configured-through-its-module/00-overview.md
deleted file mode 100644
index 8c9de102..00000000
--- a/01-RESEARCH/029-the-agent-configured-through-its-module/00-overview.md
+++ /dev/null
@@ -1,72 +0,0 @@
----
-status: graduated
-initiated: 2026-10-04
-touches:
- - 03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md
- - 03-DESIGN/00-as-is/15-the-agent-and-its-licences.md
- - 02-DECISIONS/0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md
- - 02-DECISIONS/0201-a-module-keeps-its-current-state-in-key-value-buckets-it-declares-and-reaches-through-the-runtime.md
- - 02-DECISIONS/0210-a-tools-configuration-is-its-seat-holders-and-every-other-module-extends-it-through-the-seat.md
- - 02-DECISIONS/0213-the-operator-sets-the-agents-managed-settings-through-the-agent-module.md
-became:
- - 02-DECISIONS/0216-the-agents-configuration-is-registered-through-its-module-at-three-scopes-and-served-as-one-plugin.md
- - 03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md
----
-
-# 029 — The agent configured through its module
-
-## What
-
-Everything about the operator's coding agent that can be configured is configured through the agent
-module's tools, and reaches the machines as **one plugin the mesh serves**:
-
-- subagents, skills, slash commands, hooks, output styles and tool servers;
-- the agent's settings and its instructions.
-
-Each item is registered once, from any machine, and goes to one machine, several, or all of them,
-including a machine that joins later. The mechanism is the one the module already uses for tool
-servers: the registration is kept in the module's state on the bus, and each machine's instance writes
-what applies to it. The operator chose the plugin route on the day this effort opened.
-
-**Three scopes** (the operator's direction, the same day):
-
-- **mesh:** in the mesh's plugin and the managed files, on every machine;
-- **node:** the same places, rendered for one machine or a list of them;
-- **home:** placed in the operator account's own agent directory on a machine, beside what the
- person writes there by hand.
-
-Instructions follow the same scopes: the mesh's piece, the node's piece, then further customisation per
-machine. See [03](03-options.md).
-
-## Why
-
-The vendor gives the agent a machine-wide directory for its settings, its tool servers and one
-instruction file, and **nothing machine-wide for skills, subagents, commands or hooks**. Those exist
-only in a home or a project. So today they are copied into each home by hand, and they drift and go
-stale. [01](01-what-is-configured-today.md) measures that on four machines.
-
-A plugin is the vendor's own unit for carrying all of those at once. A machine-wide setting can name
-a marketplace and enable a plugin from it. If the module serves the plugin and its own managed settings
-enable it, the mesh gets a machine-wide place for everything the vendor left home-only, and the home
-stays the person's ([ADR 0182](../../02-DECISIONS/0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md)).
-
-## What it touches
-
-- **The agent module's design** ([to-be 36](../../03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md)):
- its managed directory, its state, and its tools.
-- **Module state** ([ADR 0201](../../02-DECISIONS/0201-a-module-keeps-its-current-state-in-key-value-buckets-it-declares-and-reaches-through-the-runtime.md)):
- where registrations are kept, and which file content fits in a bucket.
-- **Contributions** ([ADR 0210](../../02-DECISIONS/0210-a-tools-configuration-is-its-seat-holders-and-every-other-module-extends-it-through-the-seat.md)):
- a module other than the agent's, say the forge's, wanting the agent to have a skill for it. That is a
- contribution to the agent's seat, not a file it writes.
-- **The managed settings key the operator sets** ([ADR 0213](../../02-DECISIONS/0213-the-operator-sets-the-agents-managed-settings-through-the-agent-module.md)),
- which a settings tool would write rather than a hand-composed settings layer.
-
-## Documents
-
-- [01 — What is configured today](01-what-is-configured-today.md): the evidence.
-- [02 — What the vendor allows](02-what-the-vendor-allows.md): plugins, marketplaces and managed
- settings, as documented, with sources.
-- [03 — Options](03-options.md): where each kind of item goes, how it is registered and stored, and
- the questions a decision has to answer.
-- [04 — What was confirmed](04-what-was-confirmed.md): the checks, tried on one workstation.
diff --git a/01-RESEARCH/029-the-agent-configured-through-its-module/01-what-is-configured-today.md b/01-RESEARCH/029-the-agent-configured-through-its-module/01-what-is-configured-today.md
deleted file mode 100644
index 50db7dbc..00000000
--- a/01-RESEARCH/029-the-agent-configured-through-its-module/01-what-is-configured-today.md
+++ /dev/null
@@ -1,51 +0,0 @@
-# 01 — What is configured today
-
-Measured on 2026-10-04 on four machines that run the agent module: two workstations, the control node
-and a home server. The figures count what sits in each operator account's agent directory in its home,
-outside the module's managed directory.
-
-## What sits in the homes
-
-| what | workstation A | workstation B | control node | home server |
-|---|---|---|---|---|
-| rule files (`rules/`) | 4 | 2 | 2 | 1 |
-| skills of the person's own (`skills/`, beside the vendor's synced ones) | 6 | 2 | 2 | 2 |
-| subagents (`agents/`) | 0 | 1 | 0 | 0 |
-| slash commands (`commands/`) | 0 | 0 | 0 | 0 |
-| plugin marketplaces known | 1 | 2 | 1 | 1 |
-
-## What that shows
-
-- **Six files the design says the operator removes are still on every machine.** To-be 36 §1 lists the
- predecessor's rule files and skills and leaves their removal to the operator, "once, on each
- workstation". On all four machines, the two predecessor skills are present, byte-identical to each
- other:
- - one that switches licences through tools that no longer exist;
- - one that names the predecessor's forge.
-
- So a session can still load a skill whose every instruction fails.
-- **One instruction, three versions.** The predecessor's node-identity rule file is on three machines,
- with three different contents. It was written per machine and then left alone.
-- **Two instruction sets that contradict each other, loaded together.** On a workstation, one session
- reads two sets of instructions:
- - the module's managed instruction file says to search the mesh's records first;
- - the predecessor's rule files in the home say to search the predecessor's knowledge base first,
- through tools that are no longer served.
-
- Both are loaded, and neither says the other is stale.
-- **A subagent exists on one machine only.** A reviewer for module definitions was written on one
- workstation. The other three machines cannot use it, and nothing says it exists.
-- **Settings are per home, and so per machine.** The agent's auto-mode environment, the rules that
- decide which actions the agent may take unasked, is written in one home's settings file. It
- describes another organisation's cloud, and it answers for this mesh's forge only through a list of
- trusted domains. When the agent refused a merge the operator had approved, the only lawful fix was
- a managed key ([ADR 0213](../../02-DECISIONS/0213-the-operator-sets-the-agents-managed-settings-through-the-agent-module.md)).
- The agent could not change its own settings, and nothing else in the mesh could either.
-
-## What already works the way this effort wants
-
-Tool servers. A server registered through the module's register tool is kept in the module's state
-on the bus, keyed `all.` or `.`. Every instance watches that state and writes what
-applies to it into the managed tool-server file, and a machine that joins later takes it at its first
-start. Today one server is registered there, for one workstation. That is the shape this effort extends
-to everything else.
diff --git a/01-RESEARCH/029-the-agent-configured-through-its-module/02-what-the-vendor-allows.md b/01-RESEARCH/029-the-agent-configured-through-its-module/02-what-the-vendor-allows.md
deleted file mode 100644
index ac201604..00000000
--- a/01-RESEARCH/029-the-agent-configured-through-its-module/02-what-the-vendor-allows.md
+++ /dev/null
@@ -1,106 +0,0 @@
-# 02 — What the vendor allows
-
-Read from the vendor's documentation on 2026-10-04; the agent installed on the machines measured in
-[01](01-what-is-configured-today.md) was a 2.1 release. Each fact names the page it came from. Where the
-documentation is silent, this says so. A fact that a design rests on is to be confirmed on one machine
-before it is built on (see [03](03-options.md), *What to confirm first*).
-
-## What a plugin can carry
-
-A plugin is a directory with a manifest in `.claude-plugin/plugin.json` and, beside it, any of:
-
-- skills, slash commands and subagents;
-- hooks;
-- tool servers (`.mcp.json`) and language servers;
-- output styles, workflows, themes and monitors;
-- a `bin/` directory;
-- a `settings.json`.
-
-Its components are namespaced by the plugin's name, so a subagent `reviewer` in a plugin `mesh` is
-`mesh:reviewer`, and it never collides with a person's own of the same name.
-— *plugins/manifest-reference, plugins/loading (name conflicts)*
-
-**What a plugin cannot carry:**
-
-- **Settings.** Only two keys of a plugin's `settings.json` take effect: the default agent and the
- subagent status line. The rest are dropped. — *plugins/manifest-reference, settings*
-- **Permission rules.** Not documented as a plugin capability.
-- **Instructions.** A `CLAUDE.md` at a plugin's root is not loaded, and the validator warns about it.
- Instructions reach a session through skills only. — *plugins/manifest-reference, standard layout*
-
-## Marketplaces, and a marketplace on the machine's own disk
-
-A marketplace is a `marketplace.json` listing plugins and where each comes from. Its sources include:
-
-- a relative path inside the marketplace;
-- a forge repository, a git URL or a subdirectory of one;
-- a package from a registry;
-- an archive over HTTPS;
-- the output of a command.
-
-**A marketplace can be a directory on the machine.** Its plugins with relative paths are **loaded in
-place**, not copied into the cache. An edit takes effect at the next session start, or at
-`/reload-plugins` in a running session, and the plugin's version need not change.
-— *plugins/marketplace-reference (marketplace sources), plugins/loading (in-place and copied plugins)*
-
-A plugin from any other source is copied into a cache in the home, under
-`plugins/cache////`. — *plugins/loading*
-
-## What managed settings do with plugins
-
-These keys work in the machine-wide managed settings file — *plugins/org*:
-
-| key | what it does |
-|---|---|
-| `extraKnownMarketplaces` | registers a marketplace on every session of the machine |
-| `enabledPlugins` | `true` installs and enables a plugin; `false` blocks and hides it at every scope. The managed value outranks every other scope |
-| `strictKnownMarketplaces`, `blockedMarketplaces` | allow-list or block-list of marketplace sources |
-| `strictPluginOnlyCustomization` | refuses skills, subagents, hooks and tool servers that come from neither a plugin nor managed settings |
-| `allowManagedHooksOnly` | runs only the hooks from managed sources |
-| `disableSideloadFlags` | blocks loading a plugin from the command line |
-| `syncClaudeAiPlugins` | stops plugins synced from the vendor's web account |
-
-**Installed without anyone being asked.** Once the settings reach a machine, the marketplace is
-registered and the plugins installed at the next session start. A non-interactive run installs them in
-the background. Managed plugins do not wait for the workspace trust prompt. — *plugins/org*
-
-## What the managed settings file honours besides
-
-`permissions` (with its default mode and the switch that disables bypassing it), `autoMode`, `hooks`,
-`env`, `model`, `statusLine`, `outputStyle`, `apiKeyHelper`, and the managed-only switches for permission
-rules, hooks and tool servers. — *managed-settings*
-
-That `autoMode` is honoured from the managed file is documented. That it changes what the agent
-refuses on these machines is still to be seen ([ADR 0213](../../02-DECISIONS/0213-the-operator-sets-the-agents-managed-settings-through-the-agent-module.md)
-left that open).
-
-## Tool servers: the exclusive file wins over a plugin's
-
-When the managed tool-server file is present, as the module writes it, it is **exclusive**: only its
-servers load. The vendor's web connectors load too when a managed key allows them. **A plugin's
-`.mcp.json` servers are blocked.** — *managed-mcp (exclusive control)*
-
-So a tool server registered through the module stays in the managed tool-server file. Putting it in
-the plugin would silently stop it loading.
-
-## Variables inside a plugin
-
-- `${CLAUDE_PLUGIN_ROOT}`: the plugin's directory.
-- `${CLAUDE_PLUGIN_DATA}`: a directory that survives updates.
-- `${CLAUDE_PROJECT_DIR}`: the project's root.
-
-These resolve in hook commands, tool and language server configuration, and the content of skills,
-subagents and commands. A plugin's declared options (`userConfig`) can be marked sensitive; the agent
-asks the person for them and stores them itself. — *plugins/manifest-reference (environment variables)*
-
-## Reload
-
-A running session does not see a changed plugin until `/reload-plugins` or a new session.
-`/reload-plugins` reloads skills, subagents, hooks and servers. It does not restart monitors.
-— *plugins/loading*
-
-## Not documented
-
-- a machine-wide directory for bare skills, subagents or commands. Only a plugin enabled by managed
- settings puts them machine-wide;
-- permission rules or instructions carried by a plugin.
diff --git a/01-RESEARCH/029-the-agent-configured-through-its-module/03-options.md b/01-RESEARCH/029-the-agent-configured-through-its-module/03-options.md
deleted file mode 100644
index 7217d7f4..00000000
--- a/01-RESEARCH/029-the-agent-configured-through-its-module/03-options.md
+++ /dev/null
@@ -1,159 +0,0 @@
-# 03 — Options
-
-The route is chosen: a plugin the mesh serves. What is left open is where each kind of item goes, how it
-is registered and kept, and what the module does about what it finds in the homes.
-
-## Scopes (the operator's direction, 2026-10-04)
-
-The plugin is not the only place the module manages. **The agent's configuration is managed at three
-scopes, and each item is registered at one of them:**
-
-| scope | where it lands | reaches |
-|---|---|---|
-| **mesh** | the mesh's plugin, and the mesh's part of the managed files | every machine running the agent, including one that joins later |
-| **node** | the same plugin and managed files, as rendered on that machine | one machine, or a list of them |
-| **home** | the operator account's own agent directory on a machine (`~/.claude`) | that account on that machine |
-
-Each machine renders its own plugin from the registrations that apply to it, so a node-scoped skill sits
-in the same `mesh` plugin as a mesh-scoped one, on that machine only. The home scope places an item
-where the person's own items live, without the plugin's prefix, as if written there by hand. The
-difference is that the mesh knows it placed the item and can change or remove it.
-
-**Instructions follow the same scopes.** The agent reads the managed instruction file first, then the
-home's instruction file and its rule files, then the project's. These are concatenated, not overridden:
-a later file does not cancel an earlier one, which is how the contradiction measured in
-[01](01-what-is-configured-today.md) came about.
-
-- **The mesh's piece** sits in the managed instruction file and is the same on every machine: how a
- session on this mesh works, and the conventions.
-- **The node's piece** sits in the same file, rendered per machine: its role, and instruction sections
- registered for it.
-- **Further customisation per machine** sits in the home: a rule file the module places, at the home
- scope, beside whatever the person writes there by hand.
-
-**What the home scope needs from [ADR 0182](../../02-DECISIONS/0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md).**
-That ADR already lets the mesh own what it places in a home and hold the rest as found. So the module
-owns each home item it placed, path by path, recorded in its state. It never writes, renames or
-removes an item it did not place. A home item with the same name as one the person made is refused
-at registration, never overwritten.
-
-## Where each kind of item goes
-
-[02](02-what-the-vendor-allows.md) puts a hard limit on the plugin: it carries skills, subagents, commands,
-hooks, output styles and language servers, but no settings, no permission rules, no instructions, and
-no tool server the exclusive managed file does not list. So there are four places, not one:
-
-| kind | goes to | why there |
-|---|---|---|
-| skills, subagents, slash commands, output styles | **the mesh's plugin** | the only machine-wide place the vendor has for them |
-| hooks | **the mesh's plugin**, with the scripts beside them | a hook's script can live in the plugin and be named through `${CLAUDE_PLUGIN_ROOT}`. A hook in the managed settings would need its script placed somewhere else |
-| tool servers | **the managed tool-server file**, as today | the exclusive file blocks a plugin's servers |
-| settings and permission rules | **the managed settings file**, beside the mesh's keys ([ADR 0213](../../02-DECISIONS/0213-the-operator-sets-the-agents-managed-settings-through-the-agent-module.md)) | a plugin's settings are dropped |
-| instructions | **the managed instruction file**, in sections | a plugin's instruction file is not loaded |
-
-The plugin is reached through two managed keys the module already owns the file for:
-`extraKnownMarketplaces`, naming a marketplace directory the module writes, and `enabledPlugins`, set to
-`true` for the mesh's plugin. Neither is the operator's to set. Like the attribution key, they are the
-mesh's keys and outrank whatever the operator sets.
-
-### Option A — one plugin
-
-Everything the mesh serves is in one plugin, `mesh`, so every invocation reads `mesh:`. That is
-simple, and the name says where an item came from.
-
-### Option B — a plugin per source
-
-One plugin for what the operator registers, and one for what other modules contribute (below). An item
-then says in its name whether a person or a module definition put it there. But the operator has two
-prefixes to remember, and an item has two owners to ask about.
-
-*Leaning:* A. Where an item came from belongs in the module's list tool, not in the item's name.
-
-## Who registers an item
-
-- **The operator, through the module's tools**, from any machine, for one, several or all of them. The
- pattern is the tool-server register tool's, extended to every kind:
- - `claude_code__list`, `_register`, `_unregister` for skills, subagents, commands, hooks, output
- styles and instruction sections;
- - `claude_code_settings_get` / `_set` and `claude_code_permission_allow` / `_deny` / `_ask` /
- `_remove` for the managed settings.
-- **Another module, through the agent's seat** ([ADR 0210](../../02-DECISIONS/0210-a-tools-configuration-is-its-seat-holders-and-every-other-module-extends-it-through-the-seat.md)).
- The forge's module wanting the agent to know how pull requests are made here contributes a skill. It
- declares the contribution in its definition, and the controller renders it to the agent's holder on
- each machine where both run. That depends on the agent module holding a seat; today it holds none.
-
-The two meet in the one plugin. A contribution and a registration with the same name are refused at
-registration, and the list tool shows the owner of each.
-
-## Where a registration is kept
-
-The tool-server registrations live in a key-value bucket the module declares
-([ADR 0201](../../02-DECISIONS/0201-a-module-keeps-its-current-state-in-key-value-buckets-it-declares-and-reaches-through-the-runtime.md)),
-keyed `all.` or `.`. Skills differ: a skill is a folder, and it can carry scripts
-and reference files beside its main file. Every message on the bus is limited to about a megabyte.
-
-1. **One value per item.** The item and its files go in one value, refused above a limit well under
- the bus's. It is simple, it fits the module's existing state, and every item measured in
- [01](01-what-is-configured-today.md) takes 20 KB or less on disk,
- the vendor's synced skills aside. But a skill with a large reference file cannot
- be registered at all.
-2. **The bus's object store for files, the bucket for the item.** Large files are stored in pieces and
- the item names them. Nothing in the mesh uses the object store yet, so ADR 0201 would need
- extending.
-3. **A repository on the forge.** The plugin is built from a repository, and registering an item is a
- commit. This is reviewable and versioned. But a register tool would have to write to the forge,
- and the forge would sit on the path to every machine.
-
-*Leaning:* 1 now, with the limit stated and checked at registration. 2 when an item outgrows it. 3
-mixes the operator's configuration into the code review cycle, which it does not need.
-
-## Where the plugin is written
-
-The module owns the managed directory, so the marketplace goes under it, written whole by the
-module's code:
-
-- the marketplace file;
-- one plugin directory beside it.
-
-It is loaded in place, so a change takes effect at the next session, or at `/reload-plugins` in a
-running one. Nothing is copied into the home.
-
-## What the module does about what it did not place
-
-[01](01-what-is-configured-today.md) found stale predecessor files on every machine. The module did not
-place those, so they are held as found (ADR 0182). It can:
-
-- **report** them: a status tool lists the home's skills, subagents, commands and rule files, says which
- the mesh placed, and names those that duplicate a mesh item or call tools no longer served;
-- **import** one on request: `claude_code__import` takes an item from one machine's home and
- registers it at a scope the operator chooses. A skill written by hand on one workstation becomes a
- mesh, node or home item in one call. Removing the original stays the person's act.
-
-`strictPluginOnlyCustomization` would make home items stop loading altogether, and home-scoped items
-with them. That is the operator's choice to make through the managed settings, not a default of the
-module.
-
-## What to confirm first, on one workstation
-
-1. A directory marketplace named in the managed settings, with its plugin enabled there, loads with
- no prompt, in place, in an interactive session and in a non-interactive one.
-2. The plugin's skills, subagents and commands are offered under `mesh:`, beside the home's own
- items, with no collision.
-3. A hook in the plugin runs, with its script found through `${CLAUDE_PLUGIN_ROOT}`.
-4. The exclusive tool-server file still loads the console, and a server in the plugin does not load,
- as documented.
-5. `autoMode` in the managed settings changes what the agent refuses (ADR 0213's open point).
-6. The account can read the marketplace in the managed directory, which root owns.
-7. The managed instruction file and a home rule file the module placed are both loaded, in that order.
-
-## Questions a decision has to answer
-
-- One plugin or one per source (leaning: one).
-- How a registration is kept, and the size limit (leaning: one value per item, with a stated limit).
-- Whether the managed settings are set through tools writing the module's state, or through the
- controller's settings layer as ADR 0213 has it. If both, which one wins on the same key.
-- Whether the agent module holds a seat, so that other modules can contribute to it.
-- The three scopes, and the home scope's ownership rule: the module owns exactly the home paths it
- placed, recorded in its state, and refuses a name the person already uses.
-- Whether settings take the same three scopes. The home's settings file is the person's own, so it is
- left out unless the operator chooses otherwise.
diff --git a/01-RESEARCH/029-the-agent-configured-through-its-module/04-what-was-confirmed.md b/01-RESEARCH/029-the-agent-configured-through-its-module/04-what-was-confirmed.md
deleted file mode 100644
index dbfe3443..00000000
--- a/01-RESEARCH/029-the-agent-configured-through-its-module/04-what-was-confirmed.md
+++ /dev/null
@@ -1,38 +0,0 @@
-# 04 — What was confirmed
-
-On 2026-10-04, on one workstation running the agent's 2.1 release, the checks [03](03-options.md)
-listed were tried with a probe. The probe was a directory marketplace holding one plugin named
-`mesh`, which carried:
-
-- a skill, a subagent and a slash command;
-- a session-start hook running a script in the plugin;
-- a tool server in the plugin's own `.mcp.json`.
-
-The managed settings were set through the agent module's `managed_settings`
-([ADR 0213](../../02-DECISIONS/0213-the-operator-sets-the-agents-managed-settings-through-the-agent-module.md)),
-on that machine's layer only, and sent by a push. The module rendered them into the managed file
-within seconds, without a restart.
-
-| # | check | result |
-|---|---|---|
-| 1 | a marketplace named in the managed settings, with its plugin enabled there, loads with no prompt, in place | **confirmed.** A non-interactive session registered the marketplace and enabled the plugin at start, with nothing asked. The plugin was not copied into the home's plugin cache and is not listed among installed plugins: it is read where it lies |
-| — | a change to the plugin needs no version bump | **confirmed.** A skill added to the plugin's directory after the first session was offered by the next one |
-| — | a running session takes the plugin without being restarted | **seen.** An interactive session that was already running when the plugin was enabled offered its skills and subagent after the operator logged in again in that session, without a restart |
-| 2 | the plugin's items are offered under its name, beside the home's | **confirmed.** `mesh:probe-skill`, `mesh:probe-agent` and the command `/mesh:probe`. A collision with a home item of the same name was not tried |
-| 3 | a hook in the plugin runs, its script found through `${CLAUDE_PLUGIN_ROOT}` | **confirmed.** The session-start hook ran its script. The vendor's validator asks for the placeholder to be quoted |
-| 4 | the exclusive tool-server file still loads the console, and a plugin's server does not | **confirmed.** The session started the console and the registered servers, and never the plugin's server |
-| 5 | `autoMode` in the managed settings changes what the agent refuses | **very likely.** With a probe rule forbidding one harmless read-only command, a session that was already running had that command refused moments after the rule was rendered, though the refusal gave no reason. It also suggests the rule reached a running session without a restart. A clean check needs a session whose only difference is the rule; the agent may not start one in its own auto mode, so it is left to the operator |
-| 6 | the account can read the managed directory, which root owns | **confirmed** by what already runs: every session reads the managed instruction file from that directory |
-| 7 | the managed instruction file and the home's are both loaded, managed first | **confirmed** by what already runs: a session lists the managed instruction file first, then the home's instruction file, then each of the home's rule files |
-
-## What this changes in the options
-
-- The plugin route works as documented, with no file copied into the home. The module's managed
- directory can hold the marketplace.
-- **A tool server stays in the managed tool-server file.** Check 4 closes that.
-- **Undoing a managed setting is not the agent's to do.** When the probe was over, the agent tried to
- clear its own machine's settings layer, and its own auto mode refused that as self-modification.
- Setting it had been allowed only because it made the agent stricter. So the tools that set the
- agent's settings and permissions are tools the operator calls, and the agent calling them for
- itself is refused by the vendor's own guard. A design must not assume an agent can tidy up after
- itself.
diff --git a/01-RESEARCH/030-backups-against-our-own-mistakes/00-overview.md b/01-RESEARCH/030-backups-against-our-own-mistakes/00-overview.md
deleted file mode 100644
index 25468bf4..00000000
--- a/01-RESEARCH/030-backups-against-our-own-mistakes/00-overview.md
+++ /dev/null
@@ -1,29 +0,0 @@
----
-status: graduated
-became: [03-DESIGN/01-to-be/43-backups-against-mistakes.md, 02-DECISIONS/0214-backups-guard-against-mistakes-and-stay-on-the-machine.md]
-initiated: 2026-10-05
-touches: [04-ISSUES/242-the-mesh-has-no-backups, 02-DECISIONS/0030-data-outlives-the-mesh-that-declared-it.md, 02-DECISIONS/0053-a-step-that-runs-on-a-schedule.md, 03-DESIGN/01-to-be/31-a-module-declares-its-fail2ban-jail.md]
----
-
-# 030 — Backups against our own mistakes
-
-**What.** How the mesh keeps restore points of its data: what is copied, how often, how long it is
-kept, full or incremental, where it lives, who runs it and how it is proven to restore.
-
-**Why.** Issue 242: nothing in the mesh backs anything up, and when one misread file dropped every
-database on the control node (issue 241), the newest copies were migration leftovers nine to twelve
-days old, found by searching a disk.
-
-**The scope is set by the operator, and it is narrow on purpose: mistakes, not disasters.** A
-backup here protects against what a person, an agent or the mesh itself does wrong — a dropped
-database, a deleted bucket, a bad migration, a file overwritten — and not against a disk dying or a
-building burning. Losing data to a disaster is an accepted risk. That removes the off-site copy, the
-cross-site transfer and the second key-holder from the problem, and leaves the part that would have
-saved the night of issue 241.
-
-**What it touches.** The store providers (each knows how to dump its own store consistently), the
-host's scheduled steps (ADR 0053, built), the node-wide composition pattern a module's jail already
-uses (to-be 31), and the operator's output channel (research 028) for saying a backup failed.
-
-Documents: [01 — what the mesh holds](01-what-the-mesh-holds.md),
-[02 — options and a proposal](02-options-and-proposal.md).
diff --git a/01-RESEARCH/030-backups-against-our-own-mistakes/01-what-the-mesh-holds.md b/01-RESEARCH/030-backups-against-our-own-mistakes/01-what-the-mesh-holds.md
deleted file mode 100644
index 2c787e1f..00000000
--- a/01-RESEARCH/030-backups-against-our-own-mistakes/01-what-the-mesh-holds.md
+++ /dev/null
@@ -1,55 +0,0 @@
-# 01 — What the mesh holds, measured 2026-10-05
-
-Four machines: the control node (hosted, holds every public service), the home server (media, home
-automation, a large ZFS pool), a workstation and a laptop. Sizes are apparent sizes, rounded.
-
-## Nothing backs anything up
-
-On every machine: no backup tool other than `rsync` and `pg_dump` is installed, no systemd timer and
-no cron line mentions a backup, dump or snapshot. Every live data directory is on ext4 except the
-home server's pool (ZFS), so a filesystem snapshot is available only there.
-
-## The control node — the data that cannot be recreated
-
-| what | size | how it changes |
-|---|---|---|
-| object store (file-sync service's files, photos) | 183 GB | slowly; files added, rarely rewritten |
-| forge (repositories, attachments, its database) | 7 GB | daily |
-| MS SQL Server databases | 5 GB | daily |
-| mail (mailboxes; accounts in postgres) | 2 GB | continuously |
-| postgres (forge, mail admin, identity, file-sync index, analytics, catalogue, licence manager) | ~2 GB | continuously |
-| file-sync service's own directory, website, analytics | ~4 GB | slowly |
-| MongoDB | 0.4 GB | daily |
-| the mesh's own records (controller, vault, module state) | ~1.5 GB | continuously |
-
-Recreatable and not worth copying: container images (210 GB), the artifact registry (40 GB — every
-artifact is rebuilt from git), a 115 GB speed-test bucket and a 7 GB pre-migration object-store copy.
-
-Free space: 833 GB on the filesystem holding the data, 2.9 TB on a second one.
-
-## The home server
-
-MS SQL Server 80 GB, postgres and a self-hosted backend platform ~3 GB, chat server 6 GB, home
-automation, network controller and time-series data each under 2 GB, and the media services'
-libraries (tens of GB, mostly cover art and metadata they re-fetch). The 89 TB media library is
-replaceable by its nature and out of scope. Free: 31 TB on the pool, 453 GB on the system disk.
-
-## The workstation and the laptop
-
-The workstation has 142 GB under its services directory and 31 GB of container volumes; the laptop
-3 GB. Mostly development; what among it is data nobody can regenerate is for each module to say.
-
-## Between the sites
-
-Control node to home server ~285 Mbit/s, home server to control node ~19 Mbit/s. Irrelevant now that
-backups stay on the machine whose data they hold, recorded because it is why an off-site copy would
-have been expensive.
-
-## What issue 241 says about the requirement
-
-- The mistake was noticed within hours. A restore point a day old would have lost a day.
-- The restore had to go *beside* the live database, not over it, and that worked well.
-- A copy of a live postgres data directory needed a throwaway server of the right version to read;
- a logical dump would have restored directly.
-- The data that survived was the data outside the dropped stores. A backup that lives inside the
- store it protects — a database's own snapshot table, a bucket's own versions — dies with a drop.
diff --git a/01-RESEARCH/030-backups-against-our-own-mistakes/02-options-and-proposal.md b/01-RESEARCH/030-backups-against-our-own-mistakes/02-options-and-proposal.md
deleted file mode 100644
index 0a40a01e..00000000
--- a/01-RESEARCH/030-backups-against-our-own-mistakes/02-options-and-proposal.md
+++ /dev/null
@@ -1,74 +0,0 @@
-# 02 — Options and a proposal
-
-## Who decides what is backed up
-
-1. **A central list** on the backup holder. Rejected: it is the attentiveness rule ADR 0030
- rejected — a store added and not listed is silently unprotected.
-2. **Each module declares its own data, a node-wide holder composes them.** The pattern of to-be 31
- (a module declares its jail; the mesh composes them per node). A store provider declares *how*
- to take a consistent copy (a dump command), a module with plain files declares *which* paths. The
- holder composes every declaration on the node into one schedule. **Proposed.**
-
-The data a module keeps in a database it gets from a provider is backed up by the provider, which
-dumps every database it serves — so a consumer declares nothing, and a new consumer is covered the
-day it is provisioned.
-
-## Full or incremental
-
-- **Databases: a full logical dump every time** (`pg_dump -Fc`, MS SQL `BACKUP DATABASE`,
- `mongodump`). A dump restores with the store's own tool into a database beside the live one —
- issue 241's recovery without the throwaway server — and is consistent, which a copy of a live data
- directory is not.
-- **Everything goes into one deduplicating repository per node** (restic or borg). Each night is a
- complete restore point, yet only changed chunks cost space: the object store's 183 GB is copied
- once, then each night adds what changed. This removes the full-vs-delta trade-off rather than
- choosing a side.
-
-Considered for the object store alone: the object store's own versioning with a lifecycle rule.
-Rejected as the only copy — it lives inside the store, and a removed bucket or data directory takes
-its versions with it.
-
-## Where
-
-On the same machine, outside every data directory the mesh manages, on a second filesystem where the
-machine has one (the control node does). Not off-site: the scope is mistakes. The repository is
-encrypted anyway (both tools require it); its key is a mesh secret (ADR 0085), so a person can
-restore without the holder.
-
-## How often, how long
-
-- **Nightly**, at a quiet hour, as a scheduled step (ADR 0053).
-- **On demand before a risky act** — a migration, a retirement, an operator's experiment — through a
- verb; the act's own tooling can call it.
-- **Kept: 14 daily, 8 weekly, 6 monthly.** A mistake is usually noticed within days, sometimes weeks
- (a deleted file nobody opens). Six months bounds the space a slowly-noticed mistake needs.
-
-Estimated cost on the control node: ~200 GB for the first night, a few GB a night after; well within
-the second filesystem's 2.9 TB.
-
-## Who runs it
-
-A node seat, **`node-backup`**, held on every machine that has data by one module (named for the tool
-it wraps). It receives the declarations, runs them, keeps the repository, and offers the verbs a
-person needs:
-
-- what is backed up here, and the last good night of each;
-- take a backup now;
-- restore one item **beside** the live one — a database to `_restore`, a path to
- `.restored-` — never over it. Swapping it in stays a person's act, as in issue 241.
-
-## How it is proven
-
-- Every run checks its own result; a failed or skipped night goes to the operator's output channel
- (research 028), not only a log.
-- Weekly: the repository's integrity check, and one database restored from the newest dump into a
- throwaway instance and counted against the live one.
-- A machine with data and no successful backup in 48 hours is a problem the mesh's status shows.
-
-## Open questions
-
-- restic or borg — both fit; restic is a single binary with no server, which suits a module.
-- Whether the workstation and laptop take part at all, or only once a module there declares data.
-- The mail spool is files and the forge has a dump command of its own; whether the forge's
- repositories are worth backing up at all when every clone is a copy (issue 241 says the forge's
- *database* is the part with no other copy).
diff --git a/01-RESEARCH/031-a-core-that-cannot-fail-silently/00-overview.md b/01-RESEARCH/031-a-core-that-cannot-fail-silently/00-overview.md
deleted file mode 100644
index 03dbed17..00000000
--- a/01-RESEARCH/031-a-core-that-cannot-fail-silently/00-overview.md
+++ /dev/null
@@ -1,66 +0,0 @@
----
-status: graduated
-initiated: 2026-10-06
-touches:
- - 00-META/how-we-build.md
- - 01-RESEARCH/017-a-mesh-that-heals-itself/00-overview.md
- - 01-RESEARCH/028-the-meshs-output-channel/00-overview.md
- - 01-RESEARCH/019-a-warm-twin-of-the-running-mesh/00-overview.md
- - 02-DECISIONS/0141-the-host-delivers-its-own-successor.md
- - 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/0224-a-provider-that-keeps-failing-a-consumer-is-a-problem-the-controller-reports.md
- - 03-DESIGN/01-to-be/06-the-controller.md
- - 03-DESIGN/01-to-be/09-the-node-lifecycle.md
- - 03-DESIGN/01-to-be/25-the-bus-on-nats.md
- - 03-DESIGN/01-to-be/30-the-mesh-updates-itself-on-a-push.md
- - 04-ISSUES/187-the-mesh-tells-nobody-when-it-stops-working/00-report.md
-became:
- - 02-DECISIONS/0227-the-core-holds-nine-rules-each-checked-and-is-built-to-them-in-six-phases.md
- - 03-DESIGN/01-to-be/45-a-core-that-cannot-fail-silently.md
----
-
-# 031 — A core that cannot fail silently
-
-**What.** The principles the mesh's core must hold — the controller, the machine host, the bus, the
-console and the path a change takes through them — so that it is fully diagnosable, monitors itself,
-heals what it knows how to heal, and upgrades itself without a person standing by. And the mechanisms
-and the order in which to build them.
-
-**Why.** The operator, 2026-10-06: *"I still notice a lot of race issues, and commands being ignored, or
-no feedback, no logs, no monitoring. Our mesh core setup must be fully diagnosable, with active
-monitoring, self-healing, self-upgradeable, self-monitoring. The core principles must be very sturdy, no
-ambiguities, clear plan of execution, fail-proof setup."*
-
-The record bears it out. In the six days to 2026-10-06, 92 issue reports were opened. Of the 48 read here
-as core failures, **every one was noticed because a person or an agent looked**, and **none was raised by
-the mesh unasked**. Four faults came back through a different door after their first fix, because each
-fix closed an instance and left its class open. One merge was skipped by the bus, and twenty-three over three
-days have no matching action; a provider failed for twenty-three hours with only its own
-journal saying so; seven databases were dropped on one unreadable file.
-
-**What it touches.** The controller (its verbs, `status`, plans, a lease), the host (its apply and
-report), the bus (its advisories and its upgrade), the console, the build path, the output channel of
-research 028, and the self-healing intent of research 017, which this effort extends from the loops that
-converge modules to the core that runs those loops. 017 deferred heartbeats, conditions and advisories
-until the bus was NATS; it is now.
-
-**Documents.**
-
-- [01 — The evidence](01-evidence.md): 48 issues classified by class of failure (races, dropped
- commands, no feedback, logs only, two writers, manual repair, self-upgrade, CI-versus-live, third
- party), with time to detect and how each was noticed.
-- [02 — Principles](02-principles.md): nine, each with what exists, what is missing and **how it is
- checked**; and the candidates weighed and not kept.
-- [03 — Mechanisms and roadmap](03-mechanisms-and-roadmap.md): conditions, watchdogs from a signals table,
- a self-check (`doctor`), the output channel, healers with a hand-act log, a controller lease and report
- sequences, staged core upgrades with rollback, a facts snapshot for merge checks, a lab replay of every
- incident; five phases, ordered by risk removed; the five largest risks today.
-
-**Graduated 2026-10-06.** The operator approved the conclusion the same day (*"do the research and
-implement it"*). The nine principles and the plan became
-[ADR 0227](../../02-DECISIONS/0227-the-core-holds-nine-rules-each-checked-and-is-built-to-them-in-six-phases.md);
-the mechanisms, the tables and the six phases became
-[to-be 45](../../03-DESIGN/01-to-be/45-a-core-that-cannot-fail-silently.md). The two measurements owed
-— the signals table's bounds and a week of the hand-act log — are Phase 0's work there, not
-preconditions of the decision.
diff --git a/01-RESEARCH/031-a-core-that-cannot-fail-silently/01-evidence.md b/01-RESEARCH/031-a-core-that-cannot-fail-silently/01-evidence.md
deleted file mode 100644
index bdf09e64..00000000
--- a/01-RESEARCH/031-a-core-that-cannot-fail-silently/01-evidence.md
+++ /dev/null
@@ -1,202 +0,0 @@
-# 01 — The evidence, classified by class of failure
-
-Every issue report opened between 2026-09-30 and 2026-10-06 that bears on the mesh's core — the
-controller, the machine host, the bus, the console, the build path — read in full and classified by
-**the class of failure**, not by the component it was found in. A component view says "fix the host";
-a class view says "the same thing is wrong in four places", which is what a principle is for.
-
-## The count
-
-| | |
-|---|---|
-| issue reports opened 2026-10-01 to 2026-10-06 | **92** (about fifteen a day) |
-| of those (and a few from the days before), classified below as core failures | **48** distinct issues |
-| classified in more than one class | 17 of 48 |
-| a fault that **came back** after a fix of the same symptom | 4 chains: 200 → 265, 230 → 264, 257 → 261 → 267, 175 → 184 → 248 |
-| noticed because a person or an agent looked — at a stalled plan, a wrong outcome, a journal, a test run by hand, a review | **48 of 48** |
-| of those, the mesh's own answer carried the fact for whoever asked (a refusal, a `status` line, a push's output) | 4 (233, 244, 259, 263) |
-| raised by the mesh to anyone, unasked | **0 of 48** |
-
-The recurrences matter most. Each fix was correct for its instance and left the class standing, so the
-same symptom came back through a different door days later. That is the measurement behind the
-operator's mandate: point fixes are converging on the instances, not on the class.
-
-## The classes
-
-Nine classes, as the mandate frames them. The table under each is the evidence; *detected* is the time
-from the fault's start to the moment anyone knew; *noticed by* is how.
-
-### (a) Races: concurrent actors without an ordering
-
-Two actors act on the same thing, and the order of arrival — not an explicit order — decides the outcome.
-
-| Issue | The two actors | Detected | Noticed by |
-|---|---|---|---|
-| 204 | an outgoing and an incoming controller both sent declarations | 2 min | a person saw a module undone |
-| 201 | a plan's push carried a controller digest older than its successor had written | 10 min crash loop | a person, nothing answered |
-| 214 | the controller rebuilding itself; the outcome reached the old one or neither | 27 min | a person asked the plan twice |
-| 219 | an older build finishing later replaced a newer one | hours | a person reading builds |
-| 234 | seven declarations arrived during an eleven-minute apply; the newest was composed from a stale view and undeclared four modules | 8 min of removals | a person, the modules were gone |
-| 254 | three plans for three merges ran at once, each asking the same builds | hours | a person, one plan stuck "building" |
-| 256 | a first machine's report landed between a module's two sends and read as stale | 7 min | a person |
-| 257, 261 | the machine's five-minute reconcile and a delivery took the apply lock in the wrong order | 5 min (257), 29 s visible undo (261) | a person |
-| 267 | a reconcile's report queued behind a delivery's apply overtook it at the controller | until a hand push | a person |
-| 265 | a push reloaded the bus's permissions while its own answer was still owed | 54 of 103 pushes over two days | a person, "did not answer in time" |
-
-**What they share.** Every one is a receiver that kept *the last thing written* rather than *the
-newest thing by an explicit order*. Declarations carry a sequence (issue 107); **reports do not**
-(267 says so: "the report does not carry the declaration's sequence number, so the digest decides").
-Plans are ordered by when they were made only since ADR 0218. Builds are ordered since issue 219.
-Controllers have no epoch, so two instances can both act (204). Ordering was added one message kind at
-a time, each after a race in it was seen.
-
-### (b) Commands silently ignored, arguments dropped
-
-| Issue | What was dropped | Effect |
-|---|---|---|
-| 244 | the console removed `node` from every mesh-seat verb's schema and call | `plan` could only refuse; **`push ` arrived empty and pushed every machine** |
-| 259 | a named push's flush sent every machine a build a policy held back | a fault met on every machine at once, not one |
-| 202 | a module whose setting was unset was *left out* of the machine | the resolver vanished from a declaration, no error |
-| 188 | a refusal inside "who is on the network" dropped a machine | 40 min, every symptom pointed elsewhere |
-| 231 | a misspelled placeholder written to a file as literal text | passes every check |
-| 241 | an unreadable contributions file read as "nobody asks" | **seven databases dropped and recreated empty** |
-| 255 | the journal verb read nothing and said "-- No entries --" | a refusal that reads as a quiet service |
-| 246 | a runtime that answered late was treated as absent | the console said modules "run nowhere" |
-
-**What they share.** A receiver that could not tell *nothing was asked* from *something was lost on the
-way*, and chose a default. In 241 and 244 the default was the most destructive reading available.
-
-### (c) Outcomes not fed back to the caller
-
-| Issue | What the caller was told | What happened |
-|---|---|---|
-| 200, 265 | "did not answer in time" | the push ran; the answer was refused by the bus |
-| 176 | the console's build tool neither waits nor registers | — |
-| 229 | `plans` answers once in prose; nothing waits for a plan | an agent went round the mesh with `curl` |
-| 230, 264 | a host stood aside for its successor and its report was cancelled | the plan waited for ever, reading `late: false` |
-| 186 | the build machine dropped 26 of 43 asks; nothing counts asks against outcomes | inferred two hours later |
-| 237 | `assign` answered "held" and "does not resolve for lack of it" in one breath | a person or agent would loop |
-
-Since 2026-10-06 the controller answers within ten seconds and keeps every call's outcome under an id
-(`calls`, issue 265). Read live the same night: **that log holds the last hundred calls in the
-controller's memory**, so a controller restart — which every merge to the controller's own repository
-causes — forgets every outcome it held. And `status`, a read-only verb, took **18 seconds** to answer,
-twice in a row, so even the health question is answered only through the "still running, ask `calls`"
-path.
-
-### (d) Failures visible only as log lines
-
-| Issue | Where it was said | For how long |
-|---|---|---|
-| 179 (recurred) | the identity provider's journal, every five seconds | **23 hours**, about 31 000 refused logins |
-| 184 | the controller's log: slow consumer, heartbeats dropped | 24 min deaf |
-| 187 | five faults in one day, each found by reading a container's log hours later | hours each |
-| 183, 217, 265 | a `Permissions Violation` line from the bus client library | days |
-| 233 | `status` said `refused`, correctly; nothing said it had lasted | 1.5 h |
-| 243 | nothing: machines silently ignored lower licence generations | until a login waited three minutes |
-| 248 | the controller's event loop stopped logging at 15:17 | hours; "status showed every plan done" |
-| 266 | nothing: a merge was skipped by the bus | **23 unmatched merges over three days** |
-| 238 | a ban list of 400 entries | the operator's own address banned for four weeks |
-
-`status` printed its all-well sentence through 179, 248 and 266. ADR 0224 made the first of those break
-it. The other two have no signal that `status` reads.
-
-### (e) State that two writers own
-
-| Issue | The two writers |
-|---|---|
-| 190, 222 | the runtime's configuration written by modules that are not the runtime, and by the controller |
-| 201 | the controller seat's row written by a successor, read by a predecessor pushed back in |
-| 239 | two definitions, in two repositories, held one module name |
-| 245 | "behind" answered by a commit comparison beside the plan that already knows |
-| 257, 261, 267 | the machine's state written by both the delivery and the five-minute reconcile |
-| 250 | a merge announced by the forge's tool and by its poll |
-| 179 | the identity provider's admin password: the mesh minted one, the database kept another |
-
-The operator's direction on 245 is the principle in their own words: *"a second answer to the same
-question is how the two came to disagree."*
-
-### (f) Manual repair needed
-
-Counted from the reports' own "what unblocked it" sections:
-
-| Repair by hand | Issues | Times |
-|---|---|---|
-| a push by hand to unstick a plan waiting on a report | 230, 257, 264, 267 | at least 4 |
-| a controller restart to recreate a missing object or let go of a held message | 208, 248 | 2 |
-| a one-off program run as the controller, outside the service | 201, 248 | 2 |
-| the identity provider's admin reset through its bootstrap command | 179 | 2 |
-| a kept file restored on a machine by hand | 233 | 1 |
-| a stuck plan closed by hand | 214, 254 | 2 |
-| a ban lifted by hand | 238 | 1 |
-| a consumer remade from now | 248 | 1 |
-
-ADR 0224 (*detected automatically, repaired where safe, loud where not*) is the first rule that turns
-one of these into a mechanism. 248's `broker consumer-reset` and 254's `plans close` turned two into
-verbs a person runs. Every other row is still a hand on a machine.
-
-### (g) Self-upgrade fragility
-
-The core updates itself: the controller rebuilds and replaces itself, the host delivers its own
-successor (ADR 0141), the runtime and the console are modules, and the bus is a module on the control
-node.
-
-| Issue | What the self-upgrade broke |
-|---|---|
-| 201 | the controller pushed back to an older build than its own successor's row |
-| 204 | two controllers both sending during a handover |
-| 213, 223 | the controller ran as a container, and a new mesh installed it so |
-| 214 | the plan that rebuilds the controller lost track of it |
-| 230, 264 | the host that stands aside loses the report of the apply that delivered it (fixed twice) |
-| 245 | a rebuild of everything replaced the bus's container: **every runtime lost the bus for a minute** |
-| 248, 266 | a controller restart is where merges go missing: most of 266's 23 lie in such windows |
-| 217 | a refused announcement crash-looped nine runtimes, closing the path that would merge the fix |
-
-A machine's first-in-line rollout (ADR 0218) protects modules. It does not protect the core from itself:
-the health a plan waits for is "reported applied", which a controller that cannot plan, a host that
-cannot report or a bus that drops messages can each satisfy. Nothing rolls back. The recovery in 201
-was the mesh's own binary run by hand from the newer image.
-
-### (h) Checks that pass in CI and fail live
-
-| Issue | The environmental fact the check did not have |
-|---|---|
-| 177 | the store-backed tests are skipped by the quick check, and the mesh runs none of a module's tests |
-| 236 | the host's declaration validation is not run by the catalogue check |
-| 262 | musl takes an NXDOMAIN for IPv6 as final; glibc does not |
-| 263 | the real machine names make a consumer's identity 23–26 characters against a bound of 20 |
-| 202 | the controller's test against the real catalogue, run by nobody until that day |
-| 228 | the host's removal has no case for a `user` — found by a review, not a test |
-
-Each check was right about the world it was given. None of them was given the mesh's world: its machine
-names, its catalogue, its host's validation, its C libraries.
-
-### (i) Third-party bugs
-
-| Issue | |
-|---|---|
-| 266 | the bus server's 2.10 release skips messages on a consumer with several filter subjects |
-| 265 | a reload of the bus's authorization forgets every reply permission already granted (documented server behaviour, read from its source) |
-| 262 | a resolver answering NXDOMAIN where NODATA is correct, met by musl's stricter reading |
-
-The lesson of 266 is not "upgrade the bus" — it is that nothing compared *what was announced* with
-*what was acted on*, so a dependency's bug was silent for three days. A defence in depth (watch the
-outcome, not the transport) would have caught it whichever layer was wrong.
-
-## What would have prevented or caught each class
-
-| Class | Would have been prevented by | Would have been caught by |
-|---|---|---|
-| (a) races | every message ordered by its writer, stale refused by every receiver | a lab replay of the interleaving |
-| (b) dropped | refusing an unknown or unreadable input by name | a schema walk over every verb |
-| (c) no feedback | answer at once with an id; outcome kept durably | a watchdog on "asked and never finished" |
-| (d) logs only | — | a condition in `status` and a notification |
-| (e) two writers | one writer per piece of state | a registry of writers checked at composition |
-| (f) manual repair | a healer for every repair done twice | a counter of hand acts |
-| (g) self-upgrade | one machine first, health-gated, rolled back | a lab upgrade with a deliberately broken build |
-| (h) CI vs live | checks fed the real mesh's facts | the same, before merge |
-| (i) third party | pinning and testing the version that runs | an end-to-end count of announced vs acted |
-
-The two columns are the principles of [02](02-principles.md). Read by count, **the "caught by" column
-is the cheapest and widest**: a watchdog and a condition would have shortened most of the 48 from "a
-person noticed" to minutes, whatever the cause.
diff --git a/01-RESEARCH/031-a-core-that-cannot-fail-silently/02-principles.md b/01-RESEARCH/031-a-core-that-cannot-fail-silently/02-principles.md
deleted file mode 100644
index 92e66132..00000000
--- a/01-RESEARCH/031-a-core-that-cannot-fail-silently/02-principles.md
+++ /dev/null
@@ -1,247 +0,0 @@
-# 02 — Principles for the core, each with how it is checked
-
-Nine principles. Each is stated as a rule a reviewer can refuse a change against, carries the classes of
-[01](01-evidence.md) it answers, says what exists already, and says **how it is checked** — the
-repository's own rule ([how-we-build §5](../../00-META/how-we-build.md)): a rule that states no check is
-indistinguishable from a wrong one.
-
-They extend, not replace, the six of [research 017](../017-a-mesh-that-heals-itself/01-the-intended-behaviour.md)
-(a loop compares with what is; healing is the ordinary path again; a repair never destroys; nothing fails
-silently; what cannot be fixed goes to an agent; correctness, not only liveness). Those are about the
-loops that converge modules. These are about **the core that runs those loops**: the controller, the
-host, the bus, the console and the path a change takes through them. 017 deferred heartbeats, conditions
-and advisories until the bus was NATS. It is now, so that deferral has expired.
-
-**The core**, for this document: the controller, the machine host and its launcher, the bus server, the
-tool runtime and the console, the build seat, and the forge's announcer of merges. Everything a change
-passes through before a module's own code runs.
-
----
-
-## P1 — One writer per piece of state
-
-Every piece of state the mesh keeps has exactly one writer, named. Anyone else who wants it changed asks
-that writer; nobody writes beside it, and nobody computes a second answer to a question it already
-answers.
-
-- **Answers:** (e), most of (a). Issues 190, 201, 204, 222, 239, 245, 250, 257/261/267.
-- **Exists:** the controller is the only writer of stream definitions (to-be 25); ADR 0222 §3 (the
- controller writes no file a seat's holder owns); the collision check at composition (no two modules
- declare one path, unit, name or package); the operator's direction on 245.
-- **Missing:** a single writer for a *machine's applied state* (the delivery and the five-minute
- reconcile both apply and both report — three issues in two days); a single *controller* (two instances
- can both act during a handover, 204: nothing holds a lease); a single announcer per event kind (250
- was found by counting duplicates by hand).
-- **How it is checked:**
- 1. A **writers table** — state kind, its writer, where it is kept — is part of the to-be design, and a
- test in each core repository asserts that the code paths that write each kind are the one named
- (by a lint over the store's write calls and the bus subjects each component publishes on, the
- latter read from the grants the controller composes: a subject two components may publish on is
- refused at composition unless the table says it is shared).
- 2. **Live:** the controller holds a **lease** (a key-value entry with a revision) and every message it
- sends carries the lease's epoch; a host refuses a declaration from an older epoch and says so. A
- probe ([03](03-mechanisms-and-roadmap.md), the self-check) asserts one lease holder and no message
- from a stale epoch in the last interval.
-
-## P2 — Everything that changes state carries its writer's order, and every receiver refuses what is older
-
-Declarations, reports, plans, builds, calls and announcements each carry `(writer, epoch, sequence)`.
-Every receiver keeps the highest it has accepted per writer, and **refuses** — with a line in the mesh's
-own words and a counter — anything older. Arrival order never decides.
-
-- **Answers:** (a). Issues 201, 204, 214, 219, 234, 256, 257, 261, 264, 267.
-- **Exists:** a declaration carries a sequence and a host keeps the newest (issue 107, to-be 25 §3); a
- newer build wins over an older one finishing later (219); a newer merge's plan supersedes an older
- one (ADR 0218 §3); a report about a declaration the mesh has moved past no longer replaces the stored
- account (267).
-- **Missing:** a **report carries no sequence** — the digest last recorded as sent decides, which is why
- each of 256, 257 and 267 needed its own rule. No epoch on the controller. A plan's state carries no
- revision, so two instances can both advance it (214).
-- **How it is checked:**
- 1. A contract test per message kind, in the receiver's repository: deliver `n`, then `n−1`; the state
- names `n` and a refusal is counted. Deliver from epoch `e−1` after `e`: refused. A new message kind
- without such a test fails a check that lists every subject the component consumes against the
- tests that name it.
- 2. **Live:** the refusals counter is a signal (P5): zero is normal, a burst is a condition naming the
- writer that sent stale.
-
-## P3 — Every command is acknowledged at once, and its outcome is kept where it can be read later
-
-A call is answered within a bound the caller can rely on — with its result, or with an id. Its outcome is
-kept **durably**, outlives the process that ran it, and can be read by id or waited on. Nothing is fired
-and forgotten, and no answer depends on what the command does to the transport carrying it.
-
-- **Answers:** (c). Issues 176, 186, 200, 229, 230, 237, 264, 265.
-- **Exists:** since 265, a seat's call answers within ten seconds or says "running" with an id, `push`
- answers before it acts, and `calls` keeps the last hundred calls and their answers; the build path
- says "asked, not waited for" and ADR 0219 §3 makes every cancel/kill leave an outcome.
-- **Missing:** `calls` lives in the controller's memory, so **a controller restart forgets every
- outcome** — and the controller restarts on every merge to its own repository. Nothing waits on a plan
- (229). Observed the night this effort began: the read-only `status` took 18 s, so the health question
- itself is answered through the "still running" path.
-- **How it is checked:**
- 1. A test that walks every verb the controller announces: each answers within `AnswerWithin`, with a
- result or an id (already partly built for 265).
- 2. A test that restarts the controller between a call and the read of its outcome: the outcome is
- still there.
- 3. **Live:** a probe calls `status` and asserts it answers *in full* within the bound; a call
- `running` for longer than its verb's declared bound is a condition (P5).
-
-## P4 — Nothing is dropped silently: an input that is unknown, unreadable or unmet is refused by name
-
-A receiver that cannot read, place or honour an input refuses it and says which, where and why. It never
-substitutes a default — above all never "empty" — for "I could not tell". An unknown argument, key,
-placeholder, seat or subject is refused, naming it.
-
-- **Answers:** (b). Issues 188, 202, 231, 241, 244, 246, 255, 259.
-- **Exists:** the host's parser refuses unknown keys (ADR 0007, 0045); an undeclared setting or endpoint
- is refused (ADR 0164, 0138); a verb takes only its declared arguments (ADR 0154) and, since 244, the
- controller and the console refuse an undeclared one by name; a seat the mesh does not answer is
- refused by name (ADR 0222 §1); ADR 0219 §3, "nothing dropped is silent".
-- **Missing:** the rule is in a dozen records and in no principle, so each new reader re-meets it. The
- destructive form — **an unreadable input read as "nothing asked", then acted on** (241) — has no
- general guard.
-- **How it is checked:**
- 1. Per component, a test that feeds each input reader an unreadable, malformed and foreign input and
- asserts a refusal, never an empty result. A reader whose error path returns an empty value fails a
- lint that the core repositories run (the shape is mechanical: an error branch that returns the
- zero value of a collection).
- 2. Schema walks for every verb (244's tests) and every placeholder namespace (231).
- 3. **Destructive deltas are braked**: a reconcile that would withdraw more than a bound of what it
- holds (one consumer, one module, a fraction set per provider) stops and raises a condition instead
- (P7). Checked by a test that empties the input and asserts nothing is withdrawn.
-
-## P5 — Every expected signal has a watchdog: absence is itself a condition
-
-Every signal the core expects on a cadence or after an act — a heartbeat, a report after a send, a
-plan's progress, a build's outcome after its ask, the controller's event loop taking something in, a
-provider's standing, an announcement turning into an action — has a declared bound. Silence past the
-bound is raised as a condition, naming what was expected, from whom, since when.
-
-- **Answers:** (c), (d), (i). Issues 179, 184, 186, 187, 230, 243, 248, 257, 264, 266, 267.
-- **Exists:** a machine's "last heard from — out of touch N m" in `node show`; ADR 0090's stuck machine
- (three identical reports); ADR 0224's provider standing, shown with its silence after thirty minutes;
- 266's catch-up of merges not acted on after ten minutes (the first true *announced-versus-acted*
- watchdog); ADR 0162's bound on a plan, which 230 found never fires (`late: false` for ever).
-- **Missing:** a list of the signals, their bounds and their owners; the plan bound working; the event
- loop's last-taken age (187); asks counted against outcomes (186); the bus's own advisories (slow
- consumer, maximum deliveries, permission violations) read as observations instead of log lines.
-- **How it is checked:**
- 1. A **signals table** — signal, emitter, cadence or trigger, bound, condition raised — kept in the
- to-be design, and compiled into the controller. A test generated from it suppresses each signal in
- turn and asserts the named condition is raised within its bound and cleared when the signal
- returns.
- 2. **Live:** the self-check (P6) reports, for every row, the age of the newest signal, so a row that
- never fires is itself visible.
-
-## P6 — The mesh checks itself continuously, against live facts, and says what it found outward
-
-The invariants the design states are probed **against the running mesh** on a schedule, not only in unit
-tests. A violation is a **condition** — durable, with since-when, evidence and who can resolve it —
-shown in `status` and **sent to the operator** through the output channel. The checker's own heartbeat is
-watched from somewhere it does not run.
-
-- **Answers:** (d), (h). Issues 177, 187, 238, 245, 253, 262.
-- **Exists:** `status` itself (assembled from reports, as how-we-build §5 requires); 017's *condition*
- shape; 028's output seat, researched only; individual live checks done by hand (262: "the live check
- stays by hand, on each resolver"); 253's measurement before the collector's first run — the one time
- in the window a check ran before the damage.
-- **Missing:** a scheduled runner, a condition store, a channel out, and a watcher's watcher.
-- **How it is checked:**
- 1. Every invariant in the to-be design that names a live check is a **probe** in the self-check's
- registry; a check over the design documents counts invariants with a stated live probe against
- those without, and the number may only go down.
- 2. The self-check publishes a heartbeat; a second machine's watcher raises "the self-check is silent"
- through a channel that does not depend on the control node.
- 3. **Live, once:** a deliberately broken invariant on a lab mesh appears in `status` and as a
- notification within one probe interval.
-
-## P7 — A known failure heals itself, under a brake, and every repair is said
-
-A failure that has been repaired by hand twice is a failure the mesh must repair itself: by running the
-ordinary path again (017 P2), never by destroying (017 P3), with a budget and a back-off, and with one
-line and one event saying what it did and why. When the budget is spent, or the only repair destroys,
-it is a condition and a notification, not a retry.
-
-- **Answers:** (f). Issues 179, 208, 214, 230, 233, 248, 254, 257, 264, 267.
-- **Exists:** ADR 0224 §5, *detected automatically, repaired where safe, loud where not*, applied to the
- identity provider's admin; the host's launcher rolls back once to known-good and then halts (ADR
- 0141); the provisioner's `holds` re-provisions what a backend lost (017/02); `broker consumer-reset`
- and `plans close` as verbs.
-- **Missing:** the general mechanism. The most frequent hand act in the window — **a push by hand to
- unstick a plan waiting on a report** — has no healer: the controller could ask the machine to report
- again (the machine knows what it applied) before waiting longer.
-- **How it is checked:**
- 1. Every hand act on the core is done through a verb that records it (who, what, why) — the **hand-act
- log**. Its count per week is a reported number; an act recorded twice for the same cause is a
- condition asking for a healer.
- 2. Each healer ships with a test that induces its failure, asserts the repair and the event, and
- asserts the brake after the budget.
-
-## P8 — The core upgrades itself one machine at a time, health-gated, and rolls back on its own
-
-A new controller, host, runtime or bus reaches one machine first; it is **healthy** only when the
-self-check's probes for that component pass there (not merely when it "reported applied"); the rest
-follow only then. A component that does not become healthy within its bound is rolled back to the last
-known good **by something other than itself**, and the rollback is said. The component being replaced is
-never the only witness of its successor's success.
-
-- **Answers:** (g). Issues 201, 204, 213, 214, 217, 230, 245, 248, 264, 266.
-- **Exists:** ADR 0218's one machine first, for modules, with "applied and current" as the gate; ADR
- 0141/0142's side-by-side host versions, known-good and the launcher's single rollback; ADR 0185's
- controller serving what it can when it is behind its seat's row; 264's report kept on disk across the
- hand-over.
-- **Missing:** a health definition per core component; a gate stronger than "reported"; rollback for
- the controller, the runtime and the bus; a lease hand-over between controllers (P1); a planned,
- rehearsed path for the bus, which is still one process on one machine and whose next upgrade (266:
- 2.10 → 2.11) is one-way.
-- **How it is checked:**
- 1. **Lab:** a deliberately broken build of each core component (one that starts and does nothing; one
- that crashes; one that cannot reach the bus) is merged on a lab mesh. Each is rolled back without a
- hand, the mesh ends on the previous build, and a condition and a notification say so.
- 2. **Live:** every core rollout leaves a record — first machine, health verdict, time to verdict,
- rolled back or not — readable through `plans`.
-
-## P9 — A check is fed the real mesh's facts before a change is merged
-
-A check whose verdict depends on the environment — names and their lengths, the machines that exist,
-the catalogue as it is, the host's validation, the C library, the server versions — runs against **the
-mesh's real facts**, exported and anonymised, before merge. A dependency's version that the mesh runs is
-the version its tests run.
-
-- **Answers:** (h), (i). Issues 177, 202, 228, 236, 262, 263, 266.
-- **Exists:** 266's `TestTheImageIsTheServerTestedHere` (the bus image's release equals the tested
- server's); ADR 0223's composition test that renders the resolver's machine list; 202's test against the real
- catalogue (run by hand).
-- **Missing:** the export of facts; a merge gate that composes every real machine's declaration with the
- change and runs the host's validation over it (which would have refused 236, 263 and 202 in their own
- pull requests); a resolver test under musl as well as glibc.
-- **How it is checked:**
- 1. The controller exports a **facts snapshot** (machines, names, assignments, seats, catalogue
- commit — no secrets, no addresses) daily; the core repositories' merge check composes every machine
- from it with the change applied and runs the host's validation; a pull request that makes any
- machine fail to compose or validate fails its check, naming the machine's role and the module.
- 2. The snapshot's age is a signal (P5).
-
----
-
-## Candidates weighed and not kept as principles
-
-- **"Every invariant has a live probe, not only a unit test"** — merged into P6; it is how P6 is built.
-- **"Environment-dependent checks run against the real mesh's facts"** — kept as P9; the third-party
- case (i) folded into it, because pinning and testing the version that runs is the same act.
-- **"Self-healing is the default"** — kept as P7 but narrowed to *known* failures, those repaired by
- hand twice. A default of healing everything heals what is not understood, which is how a repair
- destroys (241's reconcile was, in its own terms, healing).
-- **"No loop blocks on long work"** (184, 248, 175) — not a separate principle: a blocked loop is a
- signal gone silent (P5, the loop's last-taken age) and a design defect each owner fixes; stating it
- as a principle adds a rule with no mesh-wide check.
-- **"Clear plan of execution"** from the mandate — not a principle about the mesh; it is the roadmap in
- [03](03-mechanisms-and-roadmap.md), and each phase there states its own verification.
-
-## How the principles relate
-
-P2 and P1 **prevent** the races. P4 **prevents** the silent drops. P3, P5 and P6 **catch** whatever the
-first three miss, at the cost of minutes, not hours. P7 and P8 **repair**. P9 **moves** the catching
-before merge. The order of the roadmap follows from that: catching first, because it is cheapest and
-covers every class, including the ones nobody has met yet.
diff --git a/01-RESEARCH/031-a-core-that-cannot-fail-silently/03-mechanisms-and-roadmap.md b/01-RESEARCH/031-a-core-that-cannot-fail-silently/03-mechanisms-and-roadmap.md
deleted file mode 100644
index ebc3f58a..00000000
--- a/01-RESEARCH/031-a-core-that-cannot-fail-silently/03-mechanisms-and-roadmap.md
+++ /dev/null
@@ -1,221 +0,0 @@
-# 03 — Mechanisms and a phased roadmap
-
-The principles of [02](02-principles.md) need few new things. Most of the parts exist in some form;
-what is missing is the connective tissue that makes a fact the mesh already has reach someone without
-being asked. This document names the mechanisms, then orders them into phases **by risk removed per unit
-of effort**, each phase with deliverables and a verification that says it is done.
-
-Effort is given in **focused working days** of one agent-and-operator pair, at the pace the record shows
-(a located issue to a merged fix in under a day is common). The figures are for ordering, not promises.
-
-## The mechanisms
-
-### M1 — Conditions (P5, P6)
-
-017's *condition*, built: a durable fact about something the mesh owns — what is wrong, since when, the
-evidence, what was tried, who can resolve it, and whether it may clear itself. Kept in a key-value
-bucket the controller writes (one writer, P1), keyed by subject (`plan/`, `machine/`,
-`provider//`, `core/`). Raised and cleared by observation only; a person
-can **silence** one for a stated time, never resolve it. `status` becomes, first, the list of open
-conditions; the all-well sentence is "no open conditions". ADR 0224's provider standing is the first
-condition kind and moves into it unchanged.
-
-### M2 — Watchdogs from a signals table (P5)
-
-One table, compiled into the controller, of every signal the core expects. Its first rows, each from an
-issue in [01](01-evidence.md):
-
-| Signal | Bound (to be measured, then set) | Condition raised | Issue |
-|---|---|---|---|
-| machine heartbeat | 3 × interval | machine silent (asleep is a declared state, ADR 0211) | 187 |
-| report after a send | the machine's last apply duration × 3, at least 2 min | sent, not reported — then **ask the machine to report again** (M5) | 230, 257, 264, 267 |
-| plan tier progress | per tier, from build and apply durations | plan stalled at tier N, waiting on X | 214, 230, 254 |
-| controller event loop took something | 2 min while the stream has pending | controller deaf | 184, 248 |
-| merge announced → plan made or "nothing reads it" | 10 min (exists, 266) | merge never acted on | 248, 266 |
-| build asked → outcome | build's own declared timeout | ask lost | 186 |
-| call `running` → finished | the verb's declared bound | call hung | 265 |
-| provider standing repeated | 30 min (exists, ADR 0224) | provider silent | 179 |
-| bus advisories: slow consumer, maximum deliveries, permission violation | any | bus refused or dropped X for Y | 183, 187, 217, 265 |
-| self-check heartbeat | 2 × its interval, watched from a second machine | the watcher is silent | — |
-| facts snapshot age | 2 days | merge checks run on stale facts | 263 |
-
-The bus advisories are the cheapest row: the server already publishes them on its system subjects, and
-the controller only has to subscribe (read-only) and translate each into the mesh's words, naming the
-call or the consumer, as 265 now does for a refused reply.
-
-### M3 — The self-check: `doctor` (P6)
-
-A controller verb, `doctor`, and the same code run every few minutes by the controller itself. Each run
-executes the **probe registry** — the live form of the design's invariants — and raises or clears
-conditions. First probes, each an invariant that a person checked by hand in the window:
-
-- every machine's declaration composes, and every host would accept it (236, 263);
-- every holder of the mesh's resolver answers a machine name for IPv4 and NODATA for IPv6 (262);
-- every seat on record has a live holder that answers (208, 218);
-- every kept archive is held by a manifest (253 — the controller's collection command already reports it);
-- exactly one controller holds the lease (P1);
-- every durable consumer's position is near its stream's head (248's replay, 266's skip);
-- no address the mesh owns is in a ban list (238);
-- `status` answers in full within its bound (P3).
-
-`doctor` with no argument answers the last run's verdict at once (P3) and, with `run`, runs now under an
-id. Its own heartbeat is a signal (M2), watched from a second machine.
-
-### M4 — The output channel (P6)
-
-[Research 028](../028-the-meshs-output-channel/00-overview.md)'s seat, built minimally first: **one**
-channel the operator chose (028 records it), plus the desktop notifier where the operator is. A condition
-is sent when raised, once more if it lasts past a bound, and when it clears. Deduplicated by the
-condition's key. The watcher's watcher (028's open question) is the second-machine watchdog of M3,
-sending through a channel that does not pass through the control node.
-
-### M5 — Healers (P7)
-
-A healer is a registered response to one condition kind: its repair (the ordinary path again), its
-budget, its brake, and the event it emits. First healers, all from hand acts in [01](01-evidence.md) §(f):
-
-| Condition | Repair | Brake |
-|---|---|---|
-| sent, not reported | ask the machine to report what it last applied (it keeps it since 264); if that names another declaration, send again | twice, then condition |
-| plan stalled on a superseded or finished wait | close the plan with its note (`plans close`, done by the mesh) | once |
-| seat holder without its worker | raise the seat's objects again (208) | once per holder |
-| consumer far behind on a history stream | `broker consumer-reset` (248) — **only** for the consumers the table marks resettable | once, then condition |
-| provider admin refuses the mesh's secret | ADR 0224 §5 (exists) | exists |
-
-And the **hand-act log**: every repair a person makes on the core goes through a verb that records who,
-what and why. Its weekly count is the measure of P7.
-
-### M6 — Order and epochs (P1, P2)
-
-- The controller takes a **lease** in a key-value bucket before it acts and renews it; its revision is
- the **epoch** every declaration and plan write carries. A starting controller waits for the lease;
- the outgoing one stops sending when it loses it. That closes 204 and makes 201/214 detectable.
-- A **report carries the sequence** of the declaration it is about; the controller keeps the highest
- per machine and refuses older accounts by sequence, not by a digest lookup (256, 257, 267 become one
- rule).
-- **The host has one apply queue.** A delivery and the reconcile are two reasons to enqueue the same
- act; the queue applies the newest declaration once, and makes one report (257/261/267 become
- impossible rather than handled).
-- Every receiver's refusal of something stale is a counted line (P2's live check).
-
-### M7 — Staged, reversible core upgrades (P8)
-
-- **A health definition per core component**, written as probes in M3's registry: the controller
- answers `status` in bound and holds the lease; the host has reported its current declaration; the
- runtime has announced and answers a PING; the bus has every stream and every durable consumer and
- passes a request/reply round trip.
-- **The gate:** ADR 0218's first machine is judged by those probes, not by "applied".
-- **Rollback by a witness that is not the new build:** the host's launcher for the host (exists); for
- the controller, the previous controller's process kept installed beside it, re-started by the host
- when the new one does not take the lease in bound; for the runtime, the host's known-good the same
- way. Each rollback is a condition, so it is said.
-- **The bus is planned, not rolled.** A bus upgrade is a declared maintenance step: streams snapshotted,
- the step announced as a condition while it runs, every consumer's position checked after (M3). The
- one-way 2.10 → 2.11 upgrade of 266 is the first. Whether the bus should become a cluster of three so
- that it can be upgraded live is a question for its own effort.
-
-### M8 — The facts snapshot and the merge gate (P9)
-
-The controller exports a facts snapshot (machines by role and name length, assignments, seats, catalogue
-commit) to a place the build seat reads. The core repositories' and the catalogue's merge checks compose
-every machine with the change and run the host's validation. The resolver module's tests run under musl
-and glibc. A bus, store or library version the mesh runs is the one its tests run (266's pattern,
-generalised).
-
-### M9 — The lab replay (all)
-
-[Research 019](../019-a-warm-twin-of-the-running-mesh/00-overview.md)'s warm twin, or a throwaway lab of
-three containers, running **scripted replays of each incident** in the window: a reconcile due during a
-push; a host self-update during its report; a bus reload during a call; a consumer with several filters
-under mixed traffic; a missing consumer made with the server's default; an unreadable contributions
-file; a controller rebuilding itself mid-plan; two controllers at once. Each replay asserts the
-principle's outcome (refused stale, condition raised, healed, rolled back). They run on every merge to a
-core repository.
-
----
-
-## The roadmap
-
-Ordered by **risk removed per day**. Detection comes first because it covers every class at once,
-including the ones not met yet; prevention second; repair and staging third; the pre-merge and lab work
-last because they are larger and pay off over months.
-
-### Phase 0 — Finish what is in flight (2–3 days)
-
-- Land and roll out the located fixes: 244, 264, 265, 266 (including the bus's planned 2.11 upgrade, done
- as M7's first planned bus step), 267, 257/261.
-- Make `calls` durable (a key-value bucket, bounded by count and age), and bring `status` inside its own
- answer bound.
-- Start the **hand-act log** now, before anything else, so every later phase is measured against a
- baseline.
-- **Done when:** the four located core issues resolve with their live checks; a controller restart
- keeps `calls`; `status` answers in full within ten seconds.
-
-### Phase 1 — The mesh says when it is wrong (5–8 days)
-
-- M1 conditions (ADR 0224's standing moved into them); M2 watchdogs for the first eight rows; the bus
- advisories subscribed and translated; M3 `doctor` with the first probes; M4 with one channel and the
- second-machine watcher.
-- **Done when:** on a lab mesh, suppressing each signal in the table raises its condition within its
- bound and sends a notification; clearing it clears both. Live: a week of conditions read back, every
- one either real or a bound corrected.
-- **Risk removed:** every class in [01](01-evidence.md) moves from "noticed by a person, hours later" to
- "said by the mesh, minutes later".
-
-### Phase 2 — Order and one writer (5–7 days)
-
-- M6: the controller's lease and epoch; a report's sequence; the host's single apply queue; stale
- refusals counted.
-- The writers table and the signals table written into a to-be design, with their compile-time checks.
-- P4's lint for empty-on-error readers in the core repositories, and the destructive-delta brake in the
- provisioner harness.
-- **Done when:** the lab replays of 204, 257/261/267 and 241 end with "refused stale", "one report" and
- "withdrawal braked" respectively; the contract test for every consumed subject exists.
-- **Risk removed:** class (a), the largest by count, and the destructive half of (b).
-
-### Phase 3 — Healers (3–5 days)
-
-- M5's first healers and the rule that a repair done by hand twice asks for one (from the hand-act log).
-- **Done when:** a lab mesh recovers from each induced failure in the healers table with no hand, says so,
- and brakes after its budget. Live: a week in which the hand-act log has no repeat.
-
-### Phase 4 — Core upgrades that roll back (8–12 days)
-
-- M7: health definitions; the gate; rollback for the controller and the runtime; the bus as a planned
- step.
-- **Done when:** on a lab mesh, a deliberately broken build of the controller, the host and the runtime
- is each rolled back without a hand, the mesh ends on the previous build, and the rollback is a
- condition and a notification. Live: the next three core rollouts each record a health verdict.
-- **Risk removed:** class (g) — the failures that take the control path itself down.
-
-### Phase 5 — Checks before merge, and the replay suite (8–12 days, then ongoing)
-
-- M8: the facts snapshot and the compose-and-validate merge gate; the libc matrix; versions tested as
- run.
-- M9: every incident in the window as a scripted replay, run on every core merge; each new core issue
- adds its replay as its "how it is checked".
-- **Done when:** the replays of 236, 262, 263 and 266 fail on the commit before their fix and pass after;
- a new core issue cannot resolve without a replay or a stated reason why none is possible.
-
-**Total, roughly six to eight weeks of focused work**, with the first visible change — the mesh saying
-when it is wrong — inside the first two.
-
-## The five largest risks today
-
-Ranked by likelihood × damage, from the window's evidence and the mesh as read the night this effort
-began:
-
-1. **A stall nobody is told about.** A plan, a merge or a report that stops is noticed only by someone
- looking (230, 248, 257, 264, 266, 267; issue 187 still open). Every release goes through a plan.
-2. **A destructive act on a misread input.** 241 dropped seven databases on one unreadable file; 234
- undeclared four modules from a stale composition; 245's advice rebuilt everything and cut the bus;
- 253's collector would have deleted every archive. There is no general brake on a large withdrawal.
-3. **The core replacing itself with nothing to roll it back.** A controller build that starts but cannot
- plan (201, 214) halts every later change, because the controller is what plans the fix. Only the
- host has a launcher rollback.
-4. **The bus as a single, un-upgradeable process.** It runs a release that skips messages (266), its
- reload drops owed replies (265), replacing it cuts every machine off (245), and its next upgrade is
- one-way and needs a restart.
-5. **Concurrent actors with no lease or report order.** Several agent sessions, plans and reconciles act
- at once; on the night this began, four named pushes, one per machine, started within two seconds. The digests
- catch most of it now, one rule per message kind; the next message kind will not have one.
diff --git a/01-RESEARCH/032-a-module-says-how-it-is-healthy/00-overview.md b/01-RESEARCH/032-a-module-says-how-it-is-healthy/00-overview.md
deleted file mode 100644
index 1ca7159c..00000000
--- a/01-RESEARCH/032-a-module-says-how-it-is-healthy/00-overview.md
+++ /dev/null
@@ -1,85 +0,0 @@
----
-status: graduated
-initiated: 2026-10-06
-became: [02-DECISIONS/0240-a-module-says-how-it-is-healthy-and-the-node-engine-judges-it.md, 03-DESIGN/01-to-be/48-a-module-says-how-it-is-healthy.md]
-touches:
- - 03-DESIGN/01-to-be/45-a-core-that-cannot-fail-silently.md
- - 03-DESIGN/01-to-be/18-building-a-module.md
- - the module manifest
- - the node-engine
- - the release gate
----
-
-# 032 — A module says how it is healthy
-
-## What is investigated
-
-Whether, and how, every module should declare **how its own health is checked**, as a field of its
-manifest, so the mesh can judge a module the way it already judges its core parts.
-
-Today the release gate ([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)) judges a
-module on its first machine by what the mesh can see from outside: the declaration applied, no new
-condition about the module or its machine since the send, and its tools answering. It does not see
-whether the module's own service works. A container that applied cleanly and then restarts in a loop,
-a web application whose port is open but whose pages fail, a database that accepts connections and
-refuses queries — each passes the gate and is caught only through what it breaks later, if anything
-notices at all. To-be 45 names this gap ("no container state in the gate").
-
-The questions:
-
-1. **What a health declaration says.** The kinds of check a module may declare (a container's own
- health state, an HTTP request and the answer expected, a TCP connect, a command run inside the
- service, a query, a tool of the module's own that answers "healthy"), its interval, its timeout,
- how many failures in a row count, and a start period during which failure does not count.
-2. **Who runs the checks, and where the result goes.** The node-engine on the machine, the node tools,
- or the module itself; how the result reaches the controller (the report, an event, a seat verb); and
- what the release gate, the self-check and the healers do with it.
-3. **What the mesh already has to build on.** Container runtimes' own healthchecks, which many images
- ship and which the mesh neither reads nor sets today; systemd's own state for services; the tools a
- module already serves.
-4. **What "healthy" covers.** Liveness (it runs), readiness (it serves), and whether a module's health
- may depend on what it requires (a provider down makes its consumers unhealthy — said once, at the
- provider, not once per consumer).
-5. **The cost and the noise.** How often checks may run across all modules on a small machine, and how
- a check avoids the single-sample flaw the self-check already met (issue 277).
-6. **Migration.** How every catalogue module gets a declaration, what a module without one is judged
- by, and whether `module check` should require one.
-
-## Why
-
-The mesh now rolls a module out on its own, one machine first, 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
-today judged from the outside. Making each module say how its health is checked turns "applied" into
-"working", for the gate, for the self-check and for whoever asks.
-
-## What it touches
-
-The module manifest and `module check`; the node-engine, which would run or read the checks; the
-release gate and the doctor probes of to-be 45; the healers, which may restart what stays unhealthy;
-the operator's conversation (ADR 0234), which carries what stays unhealthy; and every catalogue module,
-each of which would gain a declaration.
-
-## Where it stands
-
-Evidence gathered on the live mesh, options weighed and a decision drafted, 2026-10-07:
-
-- [01 — The evidence](01-evidence.md): 125 catalogue modules, 68 of them running something long-lived
- (49 a container, 19 a service); no manifest can declare a check and nothing reads one; 19 of 73
- long-running catalogue containers carry an image check, two of which were wrong in the mesh's hands;
- every restart count is 0 because a recreate loses it, and the runtime's event history is about a minute
- long. Of seven recent incidents, liveness alone would have caught the crash loop, readiness the
- eleven-hour silent web app, and only a module's own tool the refused identity-provider admin.
-- [02 — Options](02-options.md): who runs the checks, where the result goes, liveness and readiness,
- dependency-aware health, the field's shape and the migration.
-- [03 — Recommendation](03-recommendation.md): liveness judged for every long-running resource at once;
- readiness declared per resource in a field named `health` and run by the node-engine; the state in the
- report, the condition raised by the controller on the second look, so the gate needs no new rule; a
- provider down said once at the provider; a proposed decision text with how each rule is checked, and
- the migration of the 68 modules.
-
-**Graduated 2026-10-07** on the operator's word (*"yes, turn it into a decision"*), as
-[ADR 0240](../../02-DECISIONS/0240-a-module-says-how-it-is-healthy-and-the-node-engine-judges-it.md) and
-[to-be 48](../../03-DESIGN/01-to-be/48-a-module-says-how-it-is-healthy.md). The record takes the proposed
-decision, adding: a fifth state, `held`, for a resource under a maintenance step; a ceiling of five minutes on
-grace plus failing looks, so a broken start is said inside the gate's bound; and the gate's *own health* reading
-the stated health, so a resource still starting is not yet a pass.
diff --git a/01-RESEARCH/032-a-module-says-how-it-is-healthy/01-evidence.md b/01-RESEARCH/032-a-module-says-how-it-is-healthy/01-evidence.md
deleted file mode 100644
index 6460f806..00000000
--- a/01-RESEARCH/032-a-module-says-how-it-is-healthy/01-evidence.md
+++ /dev/null
@@ -1,196 +0,0 @@
-# 01 — The evidence
-
-Measured on 2026-10-07 on the live mesh of four machines (a home server, a control node, a workstation
-and a laptop), read only: the three catalogue repositories at their trunk, the controller's and the
-node-engine's source at their trunk, each machine's container runtime and service manager, and the
-controller's own answers. Counts, not anecdotes. Machines are named by role.
-
-## 1. What the catalogue runs
-
-The three catalogue repositories hold **125 module manifests**: 113 in the main catalogue, 12 in the
-media catalogue. The photos repository holds application code and no manifest; its two instances are
-modules in the main catalogue.
-
-Each manifest sorted by the **longest-lived thing it runs** (a container that stays up first, then a
-service the manifest says is running, then the mesh's own process that stays up, then a bundle only):
-
-| What a module runs | Modules |
-|---|---|
-| at least one container that stays up | **49** |
-| no such container, but a service unit stated `running` | **19** |
-| only its bundle — tools and handlers hosted by the node tools | **50** |
-| only files, directories and packages | **7** |
-
-Underneath:
-
-| Resource | Count | Of which |
-|---|---|---|
-| container | **85** in 50 modules | 77 stay up, 6 run once (a step), 2 on a schedule; **71 distinct images** |
-| service (an existing unit put in a state) | **31** in 21 modules | 26 stated `running`, 5 stateless (the machine's lifecycle) |
-| process (the mesh's own code in a unit it writes) | **20** in 18 modules | 17 run once, 2 on a schedule, **1** stays up |
-| bundle artifact | **101** in 99 modules | |
-
-And what a check could be built from:
-
-- **50** modules declare `listens` (an endpoint, a port, a protocol) — **48 of the 49** container
- modules. A TCP or HTTP check has its target named already.
-- **53** modules declare `tools`. **One** tool in the whole catalogue is a health tool by name
- (`dbus_health`); **18** modules have a tool named `…_status`.
-- **67** modules `require` something; the most-required provisions are `route` (36 consumers),
- `x11-display` (15) and `postgres-database` (12). A database down is, today, potentially twelve
- consumers failing at once.
-- **No manifest declares a health check.** The container resource has no field for one — its fields are
- image, environment, files, ports, volumes, arguments, names, networks, capabilities, logging, the
- restart triggers and the run-once and schedule modes — and the node-engine passes the runtime no
- health option. Whatever check runs is the image's own, run by the runtime by default.
-
-## 2. What the images already ship, and what the runtime says today
-
-Every container on every machine inspected — its state, its health, its restart count, and its
-**image's** healthcheck from the image configuration:
-
-| | |
-|---|---|
-| containers on the four machines | **115** (94 running) |
-| of those, declared by the catalogue and running | **79 instances** of **73** of the 77 long-running containers (4 are assigned nowhere) |
-| long-running catalogue containers whose **image ships a HEALTHCHECK** | **19 of 73 (26 %)** |
-| …in how many modules | **7 of 45** modules with a running container (mail, the hosted database suite, a spreadsheet app, the chat client, a flow editor, the media server, the certificate authority) |
-| …modules whose every container has one | **4** |
-| running container modules with **no health state at all** | **38 of 45** |
-| catalogue containers reporting `healthy` / `unhealthy` / `starting` | **19 / 0 / 0** |
-| containers reporting `unhealthy` | 5, all exited, all on the workstation, all predating the mesh (none declared) |
-
-**Two of the nineteen image checks were wrong under the mesh's own configuration** until a catalogue fix:
-
-- the hosted database suite's studio — the framework binds the address the runtime puts in `HOSTNAME`, so
- it answered only on its network address while its image's check asked `localhost`: *unhealthy for ever
- while working* (fixed 2026-10-05);
-- the flow editor — the image's check reads its settings from a path the module mounted elsewhere: *ran
- fine but reported unhealthy forever* (fixed 2026-09-30).
-
-So 2 of 19 shipped checks (≈ 10 %) gave a false *unhealthy* in the mesh's hands. Read blindly, they would
-have failed two good builds at the gate.
-
-The intervals the images chose range from **2 s to 60 s**: a database every 2 s, three web apps every
-5 s, an API gateway every 10 s, the rest 30 s or 60 s. Start periods range from none to 360 s (the
-antivirus). Retries from 3 to 20.
-
-### The restart count the runtime keeps is lost
-
-**All 94 running containers report a restart count of 0** — including the agent server that, per
-[issue 268](../../04-ISSUES/268-letta-printed-its-passwords-into-its-log/00-report.md), had restarted about
-a hundred times in a crash loop days before. The counter belongs to a container, and the node-engine
-recreates a container whenever its declaration or a file it reads changes; the fix recreated it and the
-history went with it. A restart count is only evidence if something outside the container keeps it.
-
-### The runtime's event history is about a minute long
-
-The runtime keeps its most recent ~250 events in memory. On the home server, a query for the last 30
-minutes and for the last minute both answered ~250 events, **every one an `exec_*` event** — the image
-health checks running (84 creates, 85 dies, 84 starts). A 24-hour query for container lifecycle events,
-through the mesh's own `docker_events` tool, answered **zero**. The images' checks crowd every lifecycle
-event out of the history within a minute. A container that died an hour ago leaves no trace there.
-
-## 3. What the service manager says today
-
-- **Failed system units:** 0 on three machines; 2 on the workstation, both mounts that the mesh does not
- declare. One failed user unit on each of two machines, neither the mesh's.
-- The mesh's own units (`mesh-ca-trust`, `mesh-filter`, the power units, the controller, the node tools,
- the node-engine): all `active`, **`NRestarts` 0**.
-- systemd already gives, per unit and for free: `ActiveState`/`SubState`, `is-failed`, `NRestarts`,
- the time it entered its state, and — through a unit's own `ExecStartPost`/`WatchdogSec` where software
- supports it — readiness. None of it is read by the mesh for a module's unit.
-
-## 4. What the node-engine reports today
-
-The node-engine's report (its `Report` message) carries: what applied and what failed per resource,
-refusals, the declaration's digest and order, held and stray containers, reachable sockets on an adopted
-machine, packet filters and the firewall found, windows (a scheduled step holding containers still), the
-machine's profile, the engine's own build, outward links, rollbacks its witnesses decided, and whether it
-reads `witness`. Separately, an `Alive` heartbeat with its interval.
-
-**It carries no container state, no unit state and no health.** The controller's `node` answer for the
-home server shows the consequence: its assigned modules, its strays, its filters and its capabilities —
-and nothing about whether any of its 45 catalogue containers is running.
-
-The node-engine has one judging mechanism already: the **witness** (to-be 45 §8) — a process's `witness`
-field, `lease` or `ping` or `none`, judges a new build of the controller or the node tools and restores
-the previous one when it is not healthy in bound. Only the two core processes use it.
-
-A **run-once step** is already a health gate in practice: a step exiting non-zero fails the apply and
-everything placed after it. **23** steps exist (6 containers, 17 processes); one of them, in the hosted
-database suite, is a loop that waits up to five minutes for its analytics service's `/health` before the
-services that need it start.
-
-## 5. What the release gate judges a module by
-
-The gate on a plan's first machine ([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);
-`judgeHealth` in the controller) passes a module there when, on three judgings at least 40 s apart over at
-least two minutes, within ten minutes of the send:
-
-1. no witness on the machine put a core build back since the send;
-2. the machine's last report is current and **applied** (failed or refused is broken);
-3. **no condition** raised since the send names the module on that machine, or the machine as a whole
- (issue 281 sorted the latter out);
-4. for the controller, the node-engine and the node tools: their own health definitions (lease held and
- ready; the engine's build reported; the node tools answering the bus);
-5. for any other module **that declares tools**: the machine's node tools serve them.
-
-That is the whole of it for a catalogue module. For the 49 container modules nothing in 1–5 looks at a
-container: point 5 asks the node tools, which host the module's bundle, not its container. **A container
-that applied and then crash-loops passes all five.** To-be 45's Phase 4 note says so: *"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"*.
-
-None of the self-check's probes (D1–D13, the data and delivery probes, H-controller, H-engine, H-tools,
-H-bus, DG) reads a module's container or unit state either.
-
-## 6. The incidents, and what a declaration would have done
-
-Every incident of the last ten days where a module applied and did not work, or looked as if it did not:
-
-| Incident | What the gate and the self-check saw | Caught by, after | Would a declaration have caught it? |
-|---|---|---|---|
-| The agent server crash-looped: its database lacked the vector extension the provider never created (catalogue fix 2026-10-05) | applied; tools served (its bundle runs in the node tools, not the container); no condition | a person reading its log for another reason ([issue 268](../../04-ISSUES/268-letta-printed-its-passwords-into-its-log/00-report.md)), **about a hundred restarts**; the change readying it for the home server merged 2026-09-30, the provider's fix 2026-10-05 | **Yes, by liveness alone** — *running and not restarting* — inside the gate's ten minutes; an HTTP check on its declared `web` endpoint the same |
-| A web app accepted TCP and answered no HTTP request, its database unreachable after a firewall change ([issue 145](../../04-ISSUES/145-a-machine-reads-healthy-while-its-modules-cannot-reach-each-other/00-report.md)) | *"all doing what they were told"* | a person, **after eleven hours** | **Yes, by an HTTP readiness check** within two looks (a minute at 30 s). **Not** by a TCP check — the port was open — and not by liveness |
-| The identity provider's admin refused the secret the mesh minted; every consumer's client failed ([issue 179](../../04-ISSUES/179-an-adopted-identity-providers-admin-never-took-the-minted-secret/00-report.md)) | the server up and serving; its image ships no check | a person, the module having been broken since it moved to the mesh; **twice** (2026-10-01, again 2026-10-05) | **Only by a module's own check** — *the admin logs in* — which the module now runs itself and announces (ADR 0224). No container check sees it |
-| The studio and the flow editor read unhealthy while working (§2) | nothing: the mesh does not read health | a person reading `docker ps` | The opposite case: **a check read without proving it would have rolled back two good builds.** A declaration must be owned by the module and proved before it is trusted |
-| Two media managers refused a new recycle-bin folder; their settings step exited non-zero ([issue 279](../../04-ISSUES/279-a-folder-cannot-be-owned-by-the-account-a-module-runs-as/00-report.md)) | the apply failed → the gate failed, at once | the gate | Already caught: a step is a gate. A health declaration adds nothing here |
-| The media server's event handler threw on an empty answer and was offered each event five times ([issue 276](../../04-ISSUES/276-a-handler-that-did-its-work-was-offered-it-five-times/00-report.md)) | — | the bus watchdog (S9), `max-deliveries` | **No, and it should not**: the server was healthy; handling an event is the event contract's, not health's |
-| A provisioner runtime restarted several times at start until the overlay was up, then worked ([issue 058](../../04-ISSUES/058-a-provisioner-runtime-crash-loops-until-the-overlay-is-up/00-report.md)) | — | the lab | A warning: **a restart-counting check without a start period reads churn that stops as a crash loop** |
-
-Of the seven: **two** a declaration catches that nothing catches today (the crash loop, by liveness; the
-silent web app, by readiness), **one** only a module's own tool can catch (the admin), **one** shows what
-an unproved check does wrong, and **three** are not health at all or are already caught.
-
-## 7. Cost and noise
-
-- **Reading every container's state on a machine**, health and restart count included: one `ps` of
- 38–49 containers took **17–21 ms**; one inspect of all of them **30–37 ms** (control node and home
- server, five and three runs).
-- **What the images' own checks cost today**, from the runtime's record of each check's start and end
- (95 checks over 19 containers): **median 33 ms, slowest 138 ms**. On the home server they run
- **77 checks a minute** — 8 containers, three of them every 2–5 s — for about **5 s of exec time a
- minute**. On the control node, 11 containers at 30 s: 22 checks a minute, under 1 s.
-- **The smallest machines in this mesh** have 12 cores (the control node) and 31 GB (the laptop); the busiest runs 45 catalogue containers.
- One check per long-running resource at 30 s is **90 checks a minute** on the busiest machine — about
- 3 s of exec time a minute for in-container checks, a few hundred milliseconds for HTTP or TCP checks
- the engine makes itself. The cost is not CPU; it is that every in-container check is an `exec` in the
- runtime's event history, which on the home server is already one minute long.
-- **Single-sample noise.** [Issue 277](../../04-ISSUES/277-one-unanswered-question-was-an-urgent-alert-nobody-could-read/00-report.md):
- one DNS question unanswered on a machine starting twenty containers was raised urgent; thirty asked by
- hand a moment later were all answered. Its rule — *a finding one look can be wrong about is raised on
- the second look in a row* — is the runtime's own `retries` under another name; the images use 3 to 20.
- The gate's own three passes 40 s apart are a third form of the same idea.
-
-## 8. What this says
-
-1. **Most of the catalogue has no health anywhere.** 38 of 45 running container modules, all 19
- service-only modules and every bundle module have nothing that says *working* beyond *applied*.
-2. **What exists is not read and not owned.** 19 image checks run, 2 were wrong in the mesh's hands,
- and neither the report nor the gate nor the self-check reads any of them.
-3. **The runtime's memory is too short to rely on.** Restart counts vanish with each recreate; the
- event history is a minute long. Liveness must be observed and kept by the node-engine.
-4. **Liveness alone would have caught the worst incident; readiness the longest; only a module's own
- tool the identity provider's.** All three kinds are needed, and none is enough alone.
-5. **The cost is small and the noise is known.** Tens of milliseconds a look; the two-look rule exists.
diff --git a/01-RESEARCH/032-a-module-says-how-it-is-healthy/02-options.md b/01-RESEARCH/032-a-module-says-how-it-is-healthy/02-options.md
deleted file mode 100644
index 3d0e6aa4..00000000
--- a/01-RESEARCH/032-a-module-says-how-it-is-healthy/02-options.md
+++ /dev/null
@@ -1,141 +0,0 @@
-# 02 — Options
-
-Each question of the [overview](00-overview.md), the options for it, and what the
-[evidence](01-evidence.md) says about each. The recommendation is [03](03-recommendation.md).
-
-## A. Who runs the checks
-
-### A1 — The container runtime's own HEALTHCHECK, read by the node-engine
-
-The image's check, or one the module sets, run by the runtime inside the container; the node-engine reads
-the `health` state it keeps.
-
-- **For:** 19 images already ship one; it runs inside the container's namespace, so a check of
- `localhost` sees what the program sees; the runtime handles interval, timeout, retries and start period.
-- **Against:** containers only — nothing for the 19 service-only modules, the one long-running process,
- or anything a module's own tool knows. Every check is an `exec`, and on the home server the 77 a minute
- already push every lifecycle event out of the runtime's history (01 §2). Two of 19 shipped checks were
- wrong in the mesh's configuration; an image check the module never stated is a check nobody owns. The
- runtime does nothing when a container turns unhealthy (it restarts only on exit), so the state is only
- useful to whoever reads it.
-
-### A2 — The node-engine runs every check itself
-
-HTTP and TCP from the machine, `exec` through the runtime, a unit's state through the service manager, a
-tool through the node tools.
-
-- **For:** one runner and one reader per machine, for every hosting form; it keeps what the runtime
- forgets (restarts across recreates, 01 §2); a check from outside the container tests the path a caller
- takes, which issue 145 says matters; HTTP and TCP cost no `exec`.
-- **Against:** the engine grows a scheduler; a check that only makes sense inside the container (a CLI,
- a pid file) still needs an `exec`, which A1 does better.
-
-### A3 — The module's own tool answers "healthy"
-
-The bundle exposes a health tool; something calls it.
-
-- **For:** the only kind that catches a failure of function — the identity provider's refused admin
- (issue 179) — and the module knows what "working" means for it.
-- **Against:** a bundle is hosted by the node tools, not by the service; a tool that answers proves the
- bundle is up, not the server. One tool in the catalogue is a health tool today. And a module that judges
- itself is the thing ADR 0227 rule 8 refuses for the core: *the component being replaced is never the
- judge*. For readiness of function it is the only option; for liveness it must not be the only one.
-
-### A4 — A mix, by kind (the shape the evidence points at)
-
-The engine owns every check and every verdict. It runs HTTP, TCP and unit checks itself; it delegates an
-in-container command to the runtime by setting the container's HEALTHCHECK from the declaration (so the
-runtime's retries and start period do the timing) and reads the state; it asks a module's health tool
-through the node tools. Liveness — running and not restarting — it observes for every long-running
-resource with no declaration at all.
-
-## B. Where the result goes
-
-| Option | For | Against |
-|---|---|---|
-| **B1 — A field of the report**: per module, per long-running resource, a *state* (healthy, unhealthy, starting, unknown), since when, the failing streak and the restarts the engine counted | the report is how a machine states facts about itself; the gate already reads the last report; one place to read | a report is sent after an apply, not on a change — a container that goes bad at 03:00 waits for the next report |
-| **B2 — An event on each transition** (`module...health` changed) | immediate; the controller can keep the state | events are lost or replayed; an event alone is a sample, not a state |
-| **B3 — A condition, raised by the controller** | the gate already fails a module on a new condition naming it on that machine (01 §5, point 3); the operator's conversation (ADR 0234) and the healers (ADR 0231) already act on conditions; the two-look rule and the content rule already apply | a condition is the controller's word, raised from something — it needs B1 or B2 under it |
-| **B4 — A seat verb the controller calls** (`health `) | always current | a pull per module per judging; a machine that is slow to answer reads as unhealthy |
-
-B1 + B2 + B3 together is how the core already works: the engine states its state in the report and
-emits the change; the controller keeps the last state per machine, raises the condition on the second
-look, and clears it on the look that no longer sees it. That makes **the gate need no new rule**: an
-unhealthy module raises a condition naming it, and point 3 already holds the gate on it.
-
-## C. Liveness and readiness
-
-- **Liveness** — *it runs*: a container running and not restarted within a window; a unit `active` and
- not `failed`, `NRestarts` not climbing; a process up. Observable for every long-running resource with
- no declaration. It alone catches the worst incident of the window (the crash loop). It needs a **start
- period** and a **settle rule**, or it reads issue 058's churn-that-stops as a crash loop.
-- **Readiness** — *it serves*: the declared check passes. Only the module can say what serving means.
-- **Function** — *it does its job*: the identity provider's admin logs in. A module's own tool, and only
- for what no endpoint shows.
-
-Options: judge liveness only (cheap, no declarations, misses issue 145 and 179); judge readiness only
-(misses nothing that readiness sees, but every module must declare before anything is judged); **judge
-liveness everywhere at once and readiness where declared**, making the declaration required over a
-migration. The last keeps the gate meaningful from the first day.
-
-What the mesh **does** with each is a separate choice. Restarting a container that is unhealthy is what
-an orchestrator's liveness probe does; the runtime here does not, and a restart hides the failure the
-gate is meant to see. Options: the healers restart what stays unhealthy (ADR 0231's shape: act on what
-observation raised, say whether it worked), or nothing restarts on health and the condition reaches a
-person. The evidence has no case where a restart would have fixed anything — the crash loop was
-restarting already.
-
-## D. Dependency-aware health
-
-A module requiring a provision fails when its provider fails. With 12 consumers of the database
-provision and 36 of a route, a provider down would be a dozen conditions said once each, and a dozen
-gates failed for something none of them did.
-
-| Option | What it costs |
-|---|---|
-| D1 — Ignore it | twelve conditions for one fault; the operator learns which one matters by reading all of them; the gate blames the consumers |
-| D2 — A consumer's check names the provision it exercises; when that provider is unhealthy on the record, the consumer's finding is **held under the provider's** — said as *waiting on* the provider, not raised on its own — and its gate waits rather than fails | one condition, at the provider; needs the controller to know which provider answers which consumer — it does: it composes the grants ([issue 181](../../04-ISSUES/181-an-assignment-does-not-record-which-provider-answers-it/00-report.md) is about recording it) |
-| D3 — Order the judging: providers first, consumers only when their providers are healthy | simple, but a consumer broken on its own is not said while a provider is down, which is the moment it is most needed |
-
-D2 is how issue 281 already treats a machine-level condition: what is about the machine is the
-machine's, and is never pinned on the module the gate is kept on.
-
-## E. The manifest field
-
-Research may sketch a shape; a design doc may only name the field. Where the declaration lives:
-
-- **E1 — On the resource** (`health` on a container, a process, a service): the check belongs to the
- thing it checks; a module with three containers states three. Matches how `restart-on` and `witness`
- already sit on the resource.
-- **E2 — One per module** (a top-level `health`): one verdict, but a module of eleven containers (the
- mail module) cannot say which one is wrong.
-
-The sketch for E1 — a field named `health`, holding:
-
-| Part | Meaning | Default |
-|---|---|---|
-| kind | `runtime` (the image's own check, adopted as is), `http` (an endpoint from `listens`, a path, the status expected), `tcp` (an endpoint from `listens`), `exec` (a command in the container), `unit` (the unit's own readiness), `tool` (one of the module's tools answering) | — |
-| every | interval | 30 s; not under 10 s |
-| timeout | how long one look may take | 5 s; under `every` |
-| after | looks failing in a row before it is unhealthy | 3; **not under 2** (issue 277) |
-| grace | after a start, how long failure does not count | 60 s |
-| needs | the provision whose provider it exercises, for D2 | none |
-
-An HTTP or TCP check names an endpoint by its `listens` name, never a port or an address, so a check
-follows the machine's ports the way the endpoint does. A `runtime` kind is an explicit adoption of the
-image's check, so a module that ships one *says* it does, and `module check` can prove it on a lab.
-
-## F. Migration
-
-- **F1 — Required at once** — every long-running resource declares, or `module check` refuses. 68
- modules to touch before the next merge; nothing is judged until all are done.
-- **F2 — Liveness at once, declaration required by a date** — every long-running resource is judged by
- liveness from the first build; `module check` warns, then refuses after a stated date; a catalogue-wide
- test counts the modules still without one, and the count only goes down.
-- **F3 — Optional for ever** — modules without one are judged by liveness and tools served. The 38 of 45
- container modules with nothing today would stay at liveness.
-
-What a module without a declaration is judged by in F2 and F3: liveness of every long-running resource
-plus today's five points (01 §5). A bundle-only module (50) and a files-only module (7) need no
-declaration: they run nothing long-lived of their own, and the node tools serving their tools is their
-liveness.
diff --git a/01-RESEARCH/032-a-module-says-how-it-is-healthy/03-recommendation.md b/01-RESEARCH/032-a-module-says-how-it-is-healthy/03-recommendation.md
deleted file mode 100644
index fae63724..00000000
--- a/01-RESEARCH/032-a-module-says-how-it-is-healthy/03-recommendation.md
+++ /dev/null
@@ -1,120 +0,0 @@
-# 03 — Recommendation
-
-From the [evidence](01-evidence.md) and the [options](02-options.md): the node-engine judges every
-long-running thing a module runs — **liveness for all of them, at once, with no declaration**, and
-**readiness where the module declares how** — states it in its report, and the controller raises it as a
-condition on the second look, so the gate, the self-check and the operator's conversation act on it with
-no new rule of their own. Every catalogue module that runs something long-lived declares its check
-within a stated migration, and `module check` then requires it.
-
-## Proposed decision text
-
-Ready for graduation through playbook 02. The number is assigned then.
-
-> **A module says how it is healthy, and the node-engine judges it**
->
-> **Context.** The release gate (ADR 0236) judges a catalogue module on its first machine by what the
-> mesh sees from outside: the declaration applied, no new condition about it or its machine, its tools
-> served. None of that looks at what the module runs. Of 125 catalogue modules, 49 run a container that
-> stays up and 19 a service unit; no manifest can declare a health check, the node-engine reports no
-> container or unit state, and no probe reads one. 19 of 73 long-running catalogue containers have an
-> image check the mesh never reads, two of which were wrong in the mesh's configuration. In ten days a
-> container crash-looped about a hundred times and a web app answered nothing for eleven hours, both
-> while every check passed.
->
-> **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; it never
-> relies on the runtime's restart count or event history. A resource held still by an open window (ADR 0189) is
-> neither alive nor dead: it is said as held, and judged again when the window closes.
-> 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 (default 3, **not under 2**),
-> and a grace period after a start. An endpoint is named by its `listens` name, never by a port or an
-> address. 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 healthcheck and reads
-> the state; a tool it asks through the node tools. Nothing else on the machine judges a module.
-> 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,
-> unknown — since when, the failing streak and the counted restarts; each transition is emitted as an
-> event. The controller keeps the last state per machine and raises `module..`
-> unhealthy as a condition when two consecutive states say so, and clears it on the first that does
-> not. The release gate, unchanged, holds a module on a condition naming it; at its bound the build is
-> put back.
-> 5. **A provider down is said once, at the provider.** A 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 — said as waiting on it — and the consumer's gate waits rather than fails.
-> What a consumer finds while its provider is healthy is its own.
-> 6. **Nothing is restarted for being unhealthy.** The runtime restarts what exits, as now. A condition
-> reaches a person or a healer (ADR 0231); 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 or count below the floor, or a tool the module
-> does not serve. A lab bed applies every changed declaration and requires it healthy within its grace;
-> an image check adopted by name is proved the same way, because two of nineteen were wrong.
-> 8. **Every catalogue module that runs something long-lived declares one.** `module check` warns from
-> the decision and refuses a long-running resource without `health` after the migration's date. 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 app in a minute instead of eleven hours.
-> The identity provider's refused admin needs the module's own tool, which it now has. The node-engine
-> grows a small scheduler and a report field; the controller a condition kind; the gate nothing. 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.
-
-## How each rule is checked
-
-| Rule | Checked by |
-|---|---|
-| 1 Liveness without declaration | node-engine unit tests over a fake runtime and service manager: a container recreated keeps its counted restarts; a restart inside grace is not counted; two restarts within the settle window after grace make it unhealthy. A lab bed that replays the crash loop (a container whose program exits at start) fails the gate within its bound |
-| 2 The field and its floors | `module check` refuses each out-of-range part, with a test per refusal; a catalogue-wide test parses every `health` field |
-| 3 The engine owns the verdict | a test that a declared command becomes the container's healthcheck 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 | controller tests: one unhealthy state raises nothing and is listed unconfirmed; two raise; a healthy state clears; the gate holds a module on that condition (an existing gate test, extended with this condition's kind). A lab 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; the consumers' gates wait, not fail |
-| 6 No restart on health | a node-engine test that an unhealthy container is not restarted, recreated or stopped |
-| 7 Proved before trusted | the lab bed's run of every changed declaration on every catalogue merge; the replay of the studio's false *unhealthy* (a check that asks `localhost` while the program binds elsewhere) fails the bed, not a machine |
-| 8 Every long-running module declares | a catalogue-wide test that counts the modules with a long-running resource and no `health`: it may only go down, and is zero by the date; after it, `module check` refuses |
-
-## The migration
-
-**Today:** 68 modules run something long-lived (49 with a container, 19 with a running service only);
-57 run nothing long-lived and need no declaration.
-
-1. **Build first, without declarations.** The node-engine's liveness, the report field, the events, the
- controller's condition and its two-look rule, and the dependency hold. From this step every
- long-running resource is judged by liveness. The catalogue-wide counter starts at 68.
-2. **The seven modules whose images ship checks** adopt them by name — and, for the two that were
- wrong, the fixed configuration is what the lab proves. 19 of their containers are covered at once.
-3. **The other 42 container modules, and the containers without an image check in three of the seven,** declare an HTTP check on their declared endpoint where they serve
- HTTP, TCP where they serve something else, a command where neither shows readiness. 48 of 49
- already declare the endpoint the check needs.
-4. **The 19 service-only modules** declare the unit's own readiness, or a TCP check where the unit
- listens; most are machine software (a resolver, a time daemon, a session manager), where `active` and
- not `failed` is already the honest answer.
-5. **Modules whose function no endpoint shows** add a tool check: the identity provider (its admin logs
- in), the database provider (it can create in a consumer's database), the broker. Identified as they
- are met, not in advance.
-6. **The date:** when the counter reaches zero, or six weeks after step 1, whichever is first. From then
- `module check` refuses a long-running resource without `health`.
-
-**What a module without a declaration is judged by, until then and for ever if it runs nothing
-long-lived:** liveness of everything it runs that stays up, and the gate's five points as they stand —
-applied, no witness put it back, no new condition naming it or its machine, the core's own definitions,
-its tools served.
-
-## 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 is data the windows of ADR 0189
- already carry, and belongs to that record.
-- Health of what a module's events do (issue 276) — the event contract's, and the bus watchdog's.
-- The core's own definitions (to-be 45 §8), which stand; a core component may later declare its own
- through the same field.
diff --git a/01-RESEARCH/033-split-dns-with-a-vpn-client/00-overview.md b/01-RESEARCH/033-split-dns-with-a-vpn-client/00-overview.md
deleted file mode 100644
index 9aec126e..00000000
--- a/01-RESEARCH/033-split-dns-with-a-vpn-client/00-overview.md
+++ /dev/null
@@ -1,79 +0,0 @@
----
-status: graduated
-initiated: 2026-10-07
-became:
- - 02-DECISIONS/0247-a-machine-with-a-vpn-client-routes-names-by-domain-through-a-resolver-of-its-own.md
- - 03-DESIGN/01-to-be/50-split-dns-on-a-machine-with-a-vpn-client.md
-touches:
- - 02-DECISIONS/0223-the-mesh-has-two-resolvers-and-a-machine-lists-only-them.md
- - 02-DECISIONS/0117-a-machines-uplink-is-a-seat.md
- - 03-DESIGN/01-to-be/08-connectivity.md
- - the node-uplink seat and its holders
- - the node-engine's reconcile of a file another program rewrites
----
-
-# 033 — Split DNS with a VPN client
-
-## What is investigated
-
-How a machine of the mesh resolves three kinds of name at once — the mesh's (`*.internal`), a
-corporate network's reached through a VPN client, and public ones — when the VPN client writes
-`/etc/resolv.conf` itself.
-
-[ADR 0223](../../02-DECISIONS/0223-the-mesh-has-two-resolvers-and-a-machine-lists-only-them.md) gives
-every machine one resolver file listing the mesh's two resolvers and nothing else, written by the holder
-of the machine's uplink ([ADR 0117](../../02-DECISIONS/0117-a-machines-uplink-is-a-seat.md)). On the
-laptop, a corporate VPN client replaces that file when it connects, with its own servers and eight search
-domains. The node-engine finds the file changed and writes the mesh's back on its next reconcile. From
-then on, for the rest of the session, the corporate names do not resolve; in the minutes before it, the
-mesh's names did not. Neither program knows the other exists.
-
-The questions:
-
-1. **How the VPN client sets DNS** on Linux: does it overwrite the file, use `resolvconf`, or talk to
- systemd-resolved or NetworkManager; what it backs up and restores; whether it re-asserts its file.
-2. **What it pushes**: how many servers, which domains, over which link, and what routes.
-3. **How often the two collide** on the live mesh, and what each collision costs.
-4. **The options**: per-link routing in systemd-resolved, adding the VPN's servers to the mesh's file,
- forwarding the corporate domains from the mesh's resolvers, standing back while the VPN is up, and
- any other — and what each means for ADR 0223 and for the machines that run no VPN.
-
-## Why
-
-The operator uses the VPN for work, every working day. While it is up the laptop is either cut off from
-the mesh (agents saw lookups fail) or cut off from the corporate network, depending on which program
-wrote the file last. A rule of the mesh that a machine's own work program silently breaks — or that
-breaks the machine's owner's work — is a rule that cannot be kept as stated.
-
-A sibling decision proposed alongside this effort, ADR 0241 (machine network health), makes the
-node-engine detect and report an outside writer of the resolver file. That says the fault; this effort
-asks how it stops being one.
-
-## What it touches
-
-ADR 0223's rule that a machine lists only the mesh's resolvers, and its rejection of a local forwarder;
-ADR 0117's list of what an uplink holder declares; the networkmanager module (the uplink holder on the
-laptop); the node-engine's correction of a file changed on the machine; connectivity §2.
-
-## Where it stands
-
-Evidence gathered read-only on the laptop and options weighed, 2026-10-07:
-
-- [01 — The evidence](01-evidence.md): the VPN client always *moves* the resolver file aside and writes
- its own (two servers, eight search domains), restores its backup on disconnect, and never re-asserts;
- it does not configure systemd-resolved or NetworkManager per link on this machine. In every one of the
- nine sessions since the node-engine's journal begins, the node-engine wrote the mesh's file back within
- 1 s to 3.5 min; once the VPN client's restore of a stale backup was itself corrected.
-- [02 — Options](02-options.md): six, from systemd-resolved per-link routing to replacing the VPN
- client.
-- [03 — Recommendation](03-recommendation.md): on a machine that declares a VPN client, the uplink's
- holder runs systemd-resolved as a local router on the machine's private address, and hands it the
- VPN's servers and domains the moment the VPN client writes them; every other machine is unchanged.
- A proposed decision text with how each rule is checked.
-
-**Graduated 2026-10-07** into [ADR 0247](../../02-DECISIONS/0247-a-machine-with-a-vpn-client-routes-names-by-domain-through-a-resolver-of-its-own.md)
-and [to-be 50](../../03-DESIGN/01-to-be/50-split-dns-on-a-machine-with-a-vpn-client.md). The mechanism is
-the recommendation's: systemd-resolved routing the VPN's domains over its link. Who runs it differs, by
-the operator's decision. The resolver is a module of its own on a node seat, `node-resolver`, rather than
-part of the uplink's holder, which has two forms. The VPN client's module carries the adapter, so no
-setting declares a VPN client. The interim *defer* setting is not adopted. The record says why.
diff --git a/01-RESEARCH/033-split-dns-with-a-vpn-client/01-evidence.md b/01-RESEARCH/033-split-dns-with-a-vpn-client/01-evidence.md
deleted file mode 100644
index 4e8db227..00000000
--- a/01-RESEARCH/033-split-dns-with-a-vpn-client/01-evidence.md
+++ /dev/null
@@ -1,100 +0,0 @@
-# 01 — The evidence
-
-Gathered read-only on the laptop, 2026-10-07, with the VPN disconnected: the VPN client's installed
-files, its own log (kept since its install, about five weeks), the strings of its binaries, the
-node-engine's journal (kept for eleven days), and the machine's network state. Nothing was changed and
-nothing restarted. The corporate network's domains and addresses are not reproduced here.
-
-## The machine
-
-- **Uplink:** NetworkManager, held by the mesh's `networkmanager` module, which tells it `dns=none` and
- leaves the private network's interface (`mesh0`) unmanaged. The resolver file is the module's own,
- declared from one template: the two mesh resolvers, `options timeout:1 attempts:2 edns0`.
-- **systemd-resolved** is installed, as part of systemd, but disabled and not running. The name service
- order is `files dns … resolve [!UNAVAIL=return]`, so with resolved stopped the resolver file is the
- only DNS path.
-- **The mesh's names are not in `/etc/hosts`** any more (ADR 0148 moved every name to the resolvers):
- with the resolver file replaced, no mesh name resolves on the machine itself, nor in any container.
-- **Five containers**: two on the node-engine network (they read the machine's file as it is, at each lookup),
- three on bridges (they read it, or the runtime's embedded resolver copies its servers, when they start).
-
-## The VPN client
-
-**FortiClient 7.4, the vendor's own Linux client** (not openfortivpn), its scheduler running as a system
-service; the tunnel is set up on demand by its `vpn` process.
-
-**What it does at connect**, from its log, identical in all eleven sessions recorded:
-
-1. `Inherit local DNS: No` and `DNS service resetting interval: 0` — settings pushed with the profile.
- The first means the machine's own resolvers are *not* kept beside the corporate ones; the second that
- it never re-asserts its file during a session.
-2. It configures the tunnel device with `ip` ("fall back to using ip command") — so NetworkManager sees
- the tunnel as an external device it does not manage.
-3. **`Moving /etc/resolv.conf to /etc/resolv.conf.forticlient.backup`** — a rename, so the mesh's file,
- inode and all, becomes the backup.
-4. It writes a new `/etc/resolv.conf`: **two nameservers** (in a private RFC 1918 range, reached through
- the tunnel) and **eight search domains** (the corporate network's internal and cloud zones). Its
- binary carries the header it writes: `# Dynamic resolv.conf(5) file for glibc resolver(3) generated
- by forticlient`, and a line saying the original is backed up and restored after the VPN disconnects.
-5. **Split tunnel**: 119 routes through the tunnel — 110 host routes, mostly to a CDN's addresses, and
- nine networks including the whole of `10.0.0.0/8` and `172.16.0.0/12`. The mesh's private /24 sits
- inside the first, and survives only because a more specific route on `mesh0` wins.
-
-**At disconnect:** `Moving /etc/resolv.conf.forticlient.backup to /etc/resolv.conf` — the backup taken
-at connect is put back as it was then, whatever has happened to the file since. With no backup present
-it logs "No DNS backup file was found. Skip."
-
-**What it does not do on this machine.** Its binary knows systemd-resolved (if running, it *reads* the
-resolvers from resolved's own file and flushes resolved's cache) and NetworkManager (it can allocate the
-tunnel through `nmcli`, set a connection's DNS search domains, and drop a configuration file into
-NetworkManager's `conf.d`, keeping its own backups beside the resolver file). Here it took neither path.
-In no path found in the binary does it give systemd-resolved a per-link server or routing domain; it
-always writes `/etc/resolv.conf` itself. Public reports agree: with systemd-resolved running, the client
-replaces the stub symlink with its static file.
-
-## The collision
-
-The node-engine corrects a declared file that was "changed on the machine since this host last wrote it"
-on its next apply or reconcile. Matching its journal to the VPN client's log:
-
-| Session | VPN wrote its file | Node-engine wrote the mesh's back | Gap |
-|---|---|---|---|
-| 1 | 21:20:18 | 21:21:38 | 1 min 20 s |
-| 2 | 11:51:33 | 11:54:48 | 3 min 15 s |
-| 3 | 12:59:48 | 13:00:59 | 1 min 11 s |
-| 4 | 12:26:30 | 12:29:55 | 3 min 25 s |
-| 5 | 12:31:36 | 12:34:54 | 3 min 18 s |
-| 6 | 12:42:58 | 12:44:54 | 1 min 56 s |
-| 7 | 13:53:22 | 13:54:06 | 44 s |
-| 8 | 14:58:37 | 15:00:35 | 1 min 58 s |
-| 9 | 15:46:13 | 15:46:14 | 1 s |
-
-- **Nine of nine sessions** since the journal begins were overwritten by the mesh, within 1 s to 3.5 min
- (median about 1 min 56 s). Two earlier sessions predate the journal.
-- **Each session then ran on the mesh's file** — sessions lasted from five minutes to fourteen hours —
- so for all but the first minutes **no corporate name resolved**, while the tunnel and its routes stood.
- The VPN client never noticed: its resetting interval is 0.
-- **Sessions 4–6 were three connects within sixteen minutes**, two ended by hand after five and seven
- minutes — the shape of a person reconnecting because something stopped working.
-- **The VPN client's restore is itself an outside write.** On one night the uplink's holder changed (the
- resolver file moved from one module's template to another's) while a session was up; when the session
- ended at 02:43 the client put back its backup — the old module's file — and the node-engine corrected
- it two minutes later. A restore puts back *a* mesh file, not necessarily the current one.
-- **In the minutes before each correction**, the machine had only the corporate servers: no mesh name
- resolved (the agents' lookups failed), and public names depended on the corporate servers. The
- operator reported agents seeing `ENOTFOUND` for the AI API's public name in such a window; it is not in
- any journal kept on the machine, so how often public names failed is not measured.
-
-## What the other machines have
-
-No VPN client runs on the anchor, the home server or the workstation. The two mesh resolvers have no
-route to the corporate network's servers: they sit behind the laptop's tunnel, in ranges the laptop
-routes only to itself.
-
-## Two related faults, noted
-
-- **One mesh resolver briefly slow under load** ([issue 277](../../04-ISSUES/277-one-unanswered-question-was-an-urgent-alert-nobody-could-read/00-report.md)):
- any design that adds a hop must keep the two-resolver answer and its short timeouts.
-- **The self-check asks the resolvers only from the control node** (to-be 45 §4, D2). A machine whose
- own resolver file is wrong is invisible to it — the gap ADR 0241 (proposed) closes from the machine's
- side.
diff --git a/01-RESEARCH/033-split-dns-with-a-vpn-client/02-options.md b/01-RESEARCH/033-split-dns-with-a-vpn-client/02-options.md
deleted file mode 100644
index 7b3f3e2a..00000000
--- a/01-RESEARCH/033-split-dns-with-a-vpn-client/02-options.md
+++ /dev/null
@@ -1,113 +0,0 @@
-# 02 — Options
-
-What any answer must give, from the [evidence](01-evidence.md) and ADR 0223:
-
-- **R1** mesh names resolve on the machine and in its containers while the VPN is up;
-- **R2** the corporate domains resolve through the VPN's servers while it is up;
-- **R3** public names resolve, VPN up or down;
-- **R4** no listed set of servers can give two different answers to one name — musl asks every listed
- server at once and takes the first reply, glibc takes the first server's "no such name" as final
- ([issue 262](../../04-ISSUES/262-an-alpine-container-could-not-find-a-machine-by-its-mesh-name/00-report.md),
- ADR 0223);
-- **R5** the VPN client keeps working unmodified — it is the employer's software, configured by the
- employer's gateway; the mesh cannot change what it pushes or how it writes;
-- **R6** no change on a machine that runs no VPN.
-
-A constraint every option meets: **the VPN client always writes `/etc/resolv.conf` itself**, even with
-systemd-resolved running. No option makes it stop; each decides what happens next.
-
-## 1. systemd-resolved with per-link DNS and routing domains
-
-resolved runs on the machine. The `mesh0` link gets the two mesh resolvers with the routing domain
-`~internal` and the default route (`~.`); the tunnel link gets the VPN's two servers and its eight
-domains as routing domains. resolved sends each question to the link whose domain matches longest, the
-rest to the default route.
-
-- **R1–R3 met**, by routing rather than by listing: one answer per name.
-- **Containers cannot reach resolved's stub on `127.0.0.53`.** A container on the node-engine network can; one
- on a bridge cannot — it reads the machine's file and gets an address that is its own loopback. resolved
- can listen on further addresses (`DNSStubListenerExtra=`): on the machine's private mesh address, which
- ADR 0223 already relies on being reachable from containers on a holder. The machine's file then lists
- that one address. **R4 met**: one server listed.
-- **Who gives the tunnel link its servers?** Not the VPN client (it never sets per-link DNS) and not
- NetworkManager (the tunnel is not its device). Something must read what the VPN pushed — and the VPN
- client writes exactly that, in its file, within a second of connecting. A path watch on the resolver
- file, declared by the uplink's holder, can read the file's `nameserver` and `search` lines, give them to
- the tunnel link with `resolvectl dns` and `resolvectl domain`, and write the mesh's file back at once —
- not at the next reconcile. When the tunnel goes, resolved forgets its link's configuration by itself.
-- **The VPN client's restore at disconnect** puts back the mesh's file (or an older one: the node-engine
- corrects the content, as today). Nothing points at a link that is gone.
-- **Cost:** a daemon on the machine (already installed; part of systemd), its configuration, the watch.
-- **For ADR 0223:** it is the local forwarder 0223 rejected (its option 2), on fewer machines and with
- its three objections met: not on every machine — only on one that declares a VPN client; reachable
- from containers — on the private address; and it holds no copy of the mesh's names — every `.internal`
- question still goes to the two resolvers, so nothing can disagree with the truth.
-- **R6 met** if it is scoped to machines that declare a VPN client.
-
-## 2. The uplink's holder adds the VPN's servers to the mesh's file
-
-When the tunnel appears, the holder writes the mesh's two resolvers *and* the VPN's two servers, and the
-VPN's search domains.
-
-- **R4 broken, by construction.** The VPN's servers answer "no such name" for `.internal`; the mesh's
- resolvers forward a corporate name to the public internet, which answers "no such name" or a public
- record. glibc stops at the first listed server's answer, musl takes whichever is fastest. Whatever order
- is chosen, one kind of name fails — the exact fault ADR 0223 removed the public resolver for.
-- Rejected. It only works with something that routes by domain in front — which is option 1 or 5.
-
-## 3. The mesh's resolvers forward the corporate domains while the VPN is up
-
-- **They cannot reach the VPN's servers**: those sit behind the laptop's tunnel, routed only on the
- laptop. Forwarding through the laptop would make the mesh's resolvers depend on one laptop's session.
-- It would also put an employer's zones into the mesh's shared configuration, answered for every machine.
-- Rejected.
-
-## 4. Stand back: detect the VPN and defer
-
-While the resolver file carries the VPN client's marker and its backup holds the mesh's file, the
-node-engine does not correct it, and the machine's health says *deferred to a VPN client* rather than
-*rewritten by another program*. On disconnect the client restores the mesh's file.
-
-- **R2, R3 met; R1 broken for the whole session**: no mesh name resolves on the laptop, nor in its
- containers. The node-engine reaches the bus by name; agents and tools on the laptop lose the mesh.
-- **Cheap**: no daemon, one rule in the node-engine (and the content check on restore stays).
-- Today's behaviour is the opposite trade — R1 met after a few minutes, R2 broken for the rest of the
- session — and the evidence shows the operator living with it by reconnecting.
-- Acceptable only as a stated, chosen degradation for a machine where the owner's work comes first;
- better than the fight, worse than routing. **Useful as the fallback** of option 1 while it is not
- built, if the operator prefers work names to mesh names.
-
-## 5. A local forwarder of the mesh's own (dnsmasq) on the VPN machine
-
-As option 1, with the catalogue's resolver program in place of resolved: `server=//…`
-lines for the VPN's domains, everything else to the two mesh resolvers, listening on the private address.
-
-- Meets R1–R4 like option 1.
-- **Costs more than option 1**: a package to declare, and a configuration file to rewrite and a
- process to reload on every connect and disconnect, where resolved takes per-link settings live over its
- own interface and drops them with the link. dnsmasq is already the mesh's resolver module, which makes
- "a second copy of the resolver on this machine" easy to misread as a third mesh resolver.
-- Kept as the fallback for a machine without systemd.
-
-## 6. Replace the VPN client
-
-openfortivpn, through NetworkManager's plugin, hands the VPN's DNS to NetworkManager instead of writing
-the file; NetworkManager with `dns=systemd-resolved` then gives resolved per-link servers and domains —
-option 1 with no watch at all.
-
-- **Not the mesh's to decide** (R5): the employer's gateway may require its own client (posture checks,
- single sign-on through the vendor's flow). It would also hand the file back to NetworkManager, against
- ADR 0117's `dns=none`.
-- Recorded so the option is not rediscovered; not proposed.
-
-## Summary
-
-| | R1 mesh | R2 corporate | R3 public | R4 one answer | R6 others untouched | New on the machine |
-|---|---|---|---|---|---|---|
-| 1 resolved, per link | yes | yes | yes | yes | yes, if scoped | resolved, a path watch |
-| 2 add the VPN's servers | some | some | yes | **no** | yes | nothing |
-| 3 forward from the mesh | yes | **no** | yes | yes | **no** | nothing |
-| 4 defer | **no** | yes | yes | yes | yes | one rule |
-| 5 dnsmasq locally | yes | yes | yes | yes | yes, if scoped | a package, a reload per connect |
-| 6 replace the client | yes | yes | yes | yes | yes | not the mesh's call |
-| today (the fight) | after minutes | **no**, after minutes | mostly | yes | yes | — |
diff --git a/01-RESEARCH/033-split-dns-with-a-vpn-client/03-recommendation.md b/01-RESEARCH/033-split-dns-with-a-vpn-client/03-recommendation.md
deleted file mode 100644
index 0a4ac2de..00000000
--- a/01-RESEARCH/033-split-dns-with-a-vpn-client/03-recommendation.md
+++ /dev/null
@@ -1,89 +0,0 @@
-# 03 — Recommendation
-
-From the [evidence](01-evidence.md) and the [options](02-options.md): **route by domain on the one
-machine that runs a VPN client, and leave every other machine as ADR 0223 has it.** On a machine whose
-operator declares a VPN client, the uplink's holder runs systemd-resolved as the machine's resolver,
-listening on the machine's private address; the mesh's link carries the two mesh resolvers for the
-mesh's domain and as the default route; the moment the VPN client writes its own resolver file, the
-holder hands that file's servers and domains to the tunnel link and writes the mesh's file back. The
-fight ends because the two programs no longer want different things: the VPN's servers are used, for the
-VPN's domains only.
-
-Why not the cheaper ones: adding the VPN's servers to the file breaks the one-answer rule that ADR 0223
-exists for (option 2); the mesh's resolvers cannot reach the VPN's servers (option 3); standing back cuts
-the laptop off from the mesh for whole working days (option 4). Option 4 is kept as what the machine does
-*until* this is built, if the operator prefers it to today's fight — a choice for the operator, stated in
-the proposed text as an interim setting, not as the decision.
-
-What it asks of the sibling proposal, ADR 0241 (machine network health): on a machine that declares a VPN
-client, the VPN client's file is not an outside writer's fault but an expected handover — it is reported
-as *adopted* once the holder has taken its servers, and as a fault only if it stands longer than the
-watch's bound (a few seconds), which means the watch did not fire.
-
-## Proposed decision text
-
-Ready for graduation through playbook 02. The number is assigned then.
-
-> **A machine with a VPN client routes names by domain, and the VPN's file is taken, not fought**
->
-> **Context.** ADR 0223 gives every machine one resolver file listing the mesh's two resolvers and
-> nothing else, written by the uplink's holder (ADR 0117). A corporate VPN client on the laptop moves that
-> file aside when it connects and writes its own — two servers reached through its tunnel, eight search
-> domains — and puts its backup back on disconnect; it never re-asserts its file and never gives
-> systemd-resolved per-link settings, even when resolved runs. The node-engine wrote the mesh's file back
-> in nine of nine sessions, within 1 s to 3.5 min; from then on no corporate name resolved for the rest of
-> sessions lasting up to fourteen hours, and before it no mesh name did. No resolver file can list both
-> sets of servers: musl takes the first reply and glibc the first server's "no such name", so one kind of
-> name would fail (issue 262).
->
-> **Decision.**
->
-> 1. **A machine declares that it runs a VPN client**, as a setting of its uplink's holder, naming the
-> client. Nothing changes on a machine that does not.
-> 2. **On such a machine the uplink's holder runs systemd-resolved** as the machine's only resolver, with
-> its stub also listening on the machine's private address, so the machine's containers reach it. The
-> private network's link carries the mesh's resolvers, in ADR 0223's order, as the routing domain of the
-> mesh's names and as the default route. `/etc/resolv.conf` lists the machine's private address alone,
-> with ADR 0223's options. resolved holds no copy of any mesh name: every mesh name is asked of the two
-> resolvers, as on every other machine.
-> 3. **The VPN client's file is taken, not fought.** The holder watches the resolver file; when the
-> declared client writes it, the holder gives the tunnel link the file's servers and its search domains
-> as routing domains, and writes the mesh's file back, within seconds — not at the next reconcile. When
-> the tunnel goes, resolved drops its link's settings; the client's restore of its backup is corrected
-> by content, as any change to the file is.
-> 4. **The VPN's domains are never the mesh's.** They are not written to the mesh's store, its resolvers
-> or any other machine; they live in resolved's state for the life of the tunnel.
-> 5. **The machine says it.** The machine's network health (ADR 0241, proposed) states the VPN's file as
-> *adopted*, with the tunnel link and the count of domains routed, and raises it as an outside writer
-> only if it stands longer than the watch's bound or the client is not the declared one.
-> 6. **Until rules 2–3 run on a machine, its operator may choose to defer**: the node-engine leaves the
-> declared client's file in place while the client's backup holds the mesh's file, and the machine's
-> health says *deferred to the VPN client — mesh names do not resolve* for as long as it lasts.
->
-> **Consequences.** ADR 0223's rejection of a local forwarder stands for every machine but one that
-> declares a VPN client; there its three objections are answered — one machine, not every machine;
-> reachable from containers on the private address; no copy of the mesh's names. A machine that
-> declares a VPN client and whose resolved is down has no names; its network health says so. The
-> routes the VPN client adds are untouched: its routes include `10.0.0.0/8`, and the
-> mesh's private network keeps working only because its own route is more specific — a fact to keep in
-> mind when choosing a private range, and a check of its own.
->
-> **How it is checked.**
->
-> | Rule | Checked by |
-> |---|---|
-> | 1 Declared, nothing changes elsewhere | a controller composition test: a machine without the setting composes exactly as before; with it, the holder declares resolved and the watch |
-> | 2 resolved, on the private address, mesh link and default route | a composition test reading the holder's files; live on the laptop: `resolvectl status` shows the mesh link with `~internal` and `~.`, the stub on the private address, and the machine's file lists that address alone |
-> | 3 Taken within seconds | a lab drill: a container plays the VPN client (writes a file with its own header, servers and search lines, adds a tunnel link); the tunnel link carries the servers and domains and the mesh's file is back within 5 s; removing the link drops them |
-> | 3 Containers | the lab drill: an Alpine container on a bridge resolves a mesh name ten times out of ten while the fake tunnel is up |
-> | 4 Never the mesh's | a test that the store, the controller's renders and the other machines' files carry none of the VPN's domains after the drill |
-> | 5 Said | the machine network health test: the VPN's file read as *adopted*, not raised; raised when it stands past the bound |
-> | 6 Defer, while not built | a node-engine test: with the setting and the client's backup present, the file is not corrected and the health says deferred; without the backup it is corrected |
-> | the routes | the machine network health's check that a mesh address still routes over the private link (ADR 0241, proposed) |
-
-## What this does not decide
-
-- Which VPN client the operator uses (option 6): the employer's.
-- Resolution for a machine with *two* VPN clients — none exists.
-- Whether the operator's corporate names should resolve inside the mesh's containers on the laptop: they
- will, through the same router; restricting them is a later question if it matters.
diff --git a/01-RESEARCH/034-the-mesh-in-domains/00-overview.md b/01-RESEARCH/034-the-mesh-in-domains/00-overview.md
deleted file mode 100644
index 5400ad6b..00000000
--- a/01-RESEARCH/034-the-mesh-in-domains/00-overview.md
+++ /dev/null
@@ -1,111 +0,0 @@
----
-status: graduated
-initiated: 2026-10-07
-touches:
- - 00-META/glossary.md
- - 00-META/checks/
- - 02-DECISIONS/0006-the-substrate-and-the-control-plane.md
- - 02-DECISIONS/0008-a-context-owns-its-store.md
- - 03-DESIGN/01-to-be/06-the-controller.md
- - the descriptions of every module's tools and every seat's verbs in the catalogue
-became:
- - 02-DECISIONS/0244-the-mesh-is-described-in-domains-and-one-word-names-one-thing.md
- - 03-DESIGN/01-to-be/49-the-mesh-in-domains.md
----
-
-# 034 — The mesh in domains
-
-## What is investigated
-
-The operator, 2026-10-07: *"I wanted to develop our nox-mesh domain driven. Meaning every concept
-should fit into some domain and we try to come up with a common knowledge base/glossary/jargon for our
-application. We kind-of do this already I think, yet sometimes, you return different words for existing
-concepts."*
-
-Two terms from domain-driven design are used throughout this effort, so they are explained once here.
-A **domain** (in the literature, a *bounded context*) is an area of the system inside which every word
-has exactly one meaning, and which owns the concepts that meaning describes. A **ubiquitous language** is
-the set of words a domain uses the same way in conversation, in documents and in code. The glossary
-([`00-META/glossary.md`](../../00-META/glossary.md)) is this repository's attempt at a ubiquitous
-language for the whole mesh, without domains.
-
-The effort asks five questions:
-
-1. **Which concepts are in use**, in the glossary, the decision records, the designs and the issue
- reports, and in the words the running mesh's tools use to describe themselves?
-2. **Into which domains do they fall?** Each domain gets a purpose, the concepts it owns, the one word
- for each, and its relation to the others. The operator's starting guess — building and delivering
- changes; running modules on machines; health and alerts; data and backups; people, conversation and
- approvals; the network — is tested, not assumed.
-3. **Where do the words clash?** Two or more words for one concept (a *synonym*), or one word for two
- concepts (a *homonym*), each with evidence and a proposed single word.
-4. **How is the glossary rule checked?** [`AGENTS.md`](../../AGENTS.md) says *a rule states how it is
- checked*, and "one name per thing" is checked by nothing today.
-5. **How should the glossary be organised by domain?** Described only; the glossary itself changes when
- this effort graduates, not before.
-
-## Why
-
-Because the drift is measurable, and it costs. The glossary retired *control plane* on 2026-09-16; three
-weeks later 29 occurrences stand in 12 to-be designs — the documents that tell somebody what to do — and
-73 files created after the retirement use it. It retired *host agent*, *the host* and `mesh-host` for the
-**node-engine** on 2026-10-05; the descriptions of four tools the running mesh serves still say *"the host
-will restore"*, and the glossary's own entry for node tools says *"a host-side process the host
-supervises"*. The glossary defines **node**, while the decisions and designs of the last week use
-*machine* twice as often as *node*, and the console's own tools are called `mesh_machine` and list
-"machines". A reader — person or agent — who meets two words assumes two things, and an agent answering
-from these documents repeats whichever word it read last. That is the operator's complaint, observed.
-
-## What it touches
-
-The glossary and how it is kept; the checks in [`00-META/checks/`](../../00-META/checks/); the seven
-contexts of the controller that [ADR 0006](../../02-DECISIONS/0006-the-substrate-and-the-control-plane.md)
-named and [ADR 0008](../../02-DECISIONS/0008-a-context-owns-its-store.md) gave each its own store, which
-are the nearest thing the mesh already has to domains; and the descriptions of the tools and seat verbs
-that the modules in the catalogue serve, which are where an agent meets the mesh's words most often.
-
-## The documents
-
-| | |
-|---|---|
-| [01 — The concepts in use](01-the-concepts-in-use.md) | the inventory: what was read, how it was counted, what the glossary holds and what it lacks, and the words the running tools use |
-| [02 — The domains](02-the-domains.md) | the proposed split into ten domains plus the repository's own, each with purpose, concepts, words and relations; the operator's guess tested |
-| [03 — The clashes](03-the-clashes.md) | twenty clashes with evidence and a proposed word for each, the five most costly first |
-| [04 — How the glossary is checked](04-how-the-glossary-is-checked.md) | a proposed check over hq documents and over the catalogue's tool descriptions; described, not built |
-| [05 — A glossary by domain](05-a-glossary-by-domain.md) | how the glossary would be organised; described, not written |
-
-## Findings so far
-
-- **The mesh already has domains under another name.** ADR 0006 named seven *contexts* of the controller —
- inventory, config, connectivity, provisioning, delivery, observability, identity. Only three of them have
- a store today (`inventory`, `identity`, and `licences`, which is not one of the seven), and the words the
- mesh has grown since (seat, delivery, condition, ask, data class) are not organised by them. The
- proposed split in [02](02-the-domains.md) keeps five of the seven, renames one, and adds four.
-- **The operator's guess survives mostly intact.** Five of its six domains hold up. It lacks a domain for
- *identity and access* (credentials, grants, licences), one for *provisioning* (provider and consumer),
- the *core* the rest runs on (controller, bus, store, lease, genesis), and the *module* itself, whose
- manifest is the language every other domain reads. *Health and alerts* is better called *health and
- repair*: the mesh says *condition*, not *alert*.
-- **The glossary is short of the vocabulary in use, and contradicts itself twice.** It has 33 entries. At
- least 40 words used in more than ten decision records each — *module*, *manifest*, *assignment*,
- *machine*, *provider*, *condition*, *gate*, *tier*, *operator* among them — have no entry. It defines the
- console as a module and, further down, says node tools replaced that name; it says the deprecated broker
- holds no seat and, in the entry for *claim*, that it claims `mesh-broker`.
-- **Most drift is in two places**: governing designs written before a word was retired and never
- revisited, and tool descriptions, which nothing compares with the glossary at all.
-- **Homonyms cost more than synonyms**, and a word list cannot catch them. *Plan*, *push*, *release*,
- *gate*, *tier*, *ask*, *store* and *record* each mean two to four things today, several of them in the
- verbs of the same seat.
-
-## Questions for graduation
-
-- **Machine or node.** One concept, two words, both heavily used; [03](03-the-clashes.md) recommends
- *machine* in prose and keeps `node` only as an identifier (a scope's value and the `node-` prefix of seat
- names). This is the largest rename the effort proposes, and the operator's call.
-- **Domain or context.** The repository already says *context* (ADR 0006, ADR 0008) for an owner of
- records with its own store. Using *domain* for the vocabulary partition beside it would be the effort's
- own synonym. [02](02-the-domains.md) proposes that a domain is the vocabulary and ownership boundary and
- a context is one store inside it, and asks for that to be decided rather than left to usage.
-- **Which documents the check holds to the list**, and from which date ([04](04-how-the-glossary-is-checked.md)).
-- **Where the tool-description check runs**: in the catalogue repository's merge check, or as a probe of the
- running mesh, or both.
diff --git a/01-RESEARCH/034-the-mesh-in-domains/01-the-concepts-in-use.md b/01-RESEARCH/034-the-mesh-in-domains/01-the-concepts-in-use.md
deleted file mode 100644
index 400cff3b..00000000
--- a/01-RESEARCH/034-the-mesh-in-domains/01-the-concepts-in-use.md
+++ /dev/null
@@ -1,148 +0,0 @@
-# 01 — The concepts in use
-
-What was read, how words were counted, what the glossary holds, what it lacks, and which words the
-running mesh's tools use. The domains in [02](02-the-domains.md) and the clashes in
-[03](03-the-clashes.md) are drawn from this.
-
-## What was read
-
-| Source | Size on 2026-10-07 | How |
-|---|---|---|
-| the glossary | 33 entries | read whole |
-| decision records (`02-DECISIONS/`) | 225 records | counted; the seven-contexts record (0006), its store rule (0008) and the records from 0220 to 0240 read for their words |
-| to-be designs (`03-DESIGN/01-to-be/`) | 49 documents | counted; the controller (06), connectivity (08), and 45 to 48 read for their words |
-| as-is designs (`03-DESIGN/00-as-is/`) | 17 documents | counted |
-| issue reports (`04-ISSUES/`) | 291 issues | counted |
-| research (`01-RESEARCH/`) | 33 efforts | counted; research 005 (*which domains the catalogue groups into*) read whole, as prior art |
-| the running mesh's tools | 88 modules serving 535 tools, plus the verbs of 15 seats every machine holds and 5 seats held once for the mesh | listed through the console's read tools (overview, machine, search, describe); nothing was called that changes anything |
-
-**How words were counted.** A case-insensitive, whole-word match per file; the number given is the number
-of *files* containing the word, not of occurrences, unless a statement says otherwise. Where a file's age
-matters, its age is the date git first records it. The tool descriptions are the first line each tool
-gives of itself in the console's listing, which is cut at about 160 characters, so a word that occurs only
-late in a long description is undercounted there.
-
-## What the glossary holds
-
-Thirty-three entries, in seven sections: the mesh and its machines; what runs the mesh; what the mesh
-stores and serves; how modules relate to the mesh; the surfaces; how a change reaches the machines; the
-operator's machine. Retired words are marked three ways — struck through (`~~flavor~~`), *"Replaces
-**x**"*, and *"Not x"* — so no program can read the list of retired words from it reliably today.
-
-**It contradicts itself in two places.**
-
-- **The console.** The surfaces section defines the console as *"the module (`mesh-console`) that puts the
- mesh's tools in front of whoever is on a machine"*. The operator's machine section, further down, defines
- node tools as the runtime whose *"serving mode on loopback is what was called **the console**"*, and
- says node tools *"replaces 'console' as the module's name; console remains the word for the person's end
- of it."* Both entries are current.
-- **The deprecated broker.** Its entry says it has *"no seat, not foundation"*. The entry for *claim*
- says *"the `mesh-controller`, `postgres` and `lavinmq` modules claim the `mesh-controller`, `mesh-store`
- and `mesh-broker` seats"*, and the entry for *bus* says the bus is held by the `mesh-broker` seat.
-
-**It uses a word it retired.** The node tools entry says *"a host-side process the host supervises"*,
-eight lines after the node-engine entry retired *the host*.
-
-## What the glossary lacks
-
-Words used in more than ten decision records each, with no entry of their own (files containing each, of
-225 records):
-
-| Word | Records | Word | Records | Word | Records |
-|---|---|---|---|---|---|
-| machine | 177 | operator | 125 | catalogue | 120 |
-| declaration | 106 | manifest | 102 | reach | 101 |
-| provider | 95 | person | 91 | adopted | 84 |
-| tool | 76 | holder | 75 | grant | 65 |
-| assignment | 60 | secret | 56 | scope | 55 |
-| apply | 54 | resolver | 51 | genesis | 46 |
-| verb | 45 | firewall | 43 | route | 42 |
-| converged | 40 | private network | 37 | connectivity | 34 |
-| the lab | 33 | proxy | 31 | overlay | 30 |
-| upgrade | 27 | gate | 25 | condition | 23 |
-| endpoint | 23 | reconcile | 22 | vault | 20 |
-| hub | 19 | finding | 19 | health | 19 |
-| licence | 16 | probe | 15 | tier | 14 |
-| membership | 14 | anchor | 14 | backup | 11 |
-
-*Module* itself — the word the whole mesh is built on — has no entry. Nor do the words of the four
-designs written in the last week: *condition*, *healer*, *self-check*, *watchdog*, *hand-act*, *drill*,
-*lease*, *epoch* (to-be 45, *a core that cannot fail silently*); *data class*, *restore point* (to-be 43
-and ADR 0233); *converged* and *adopted* as the two states a machine is in (the controller's `nodes` verb
-answers with them).
-
-## The seven contexts, as they stand
-
-ADR 0006 split the controller into seven **contexts** — areas of record, each owning its own store
-(ADR 0008) — and to-be 06 tabulates them:
-
-| Context | Owns, in ADR 0006's words | A store of its own today? |
-|---|---|---|
-| inventory | nodes, modules, assignments, versions | yes |
-| config | settings, secrets, and deriving them onto nodes | no |
-| connectivity | overlay, resolution, exposure, filtering, certificates | no |
-| provisioning | resource grants between modules | no |
-| delivery | source to artifact to node | no — deliveries are kept by the `mesh-delivery` module since ADR 0239 |
-| observability | health, logs, metrics, alerts | no |
-| identity | agents, humans, services, authorisation | yes |
-
-The store has a third database the seven do not name, `licences` (ADR 0183). The seven are the nearest
-thing the mesh has to domains, and they were drawn from one question — *does answering this need more than
-one node?* — which is a test for what the controller owns, not for where a word belongs. Seat, claim,
-bench, delivery group, condition, ask, data class and every word of the operator's conversation arrived
-after them, and none of those records says which context its words belong to.
-
-Research 005 asked a neighbouring question in August — which *modules* group into domains — measured which
-modules change together, and found that only the reachability modules do. Its conclusion, recorded in ADR
-0009, was that modules are not grouped into packages; it did not touch the vocabulary. This effort groups
-*concepts and words*, not modules: a module may serve several domains, and the domains are not folders in
-the catalogue.
-
-## The words the running tools use
-
-The console's listing gives each tool's first line. Over the 535 module tools (the seat verbs are counted
-separately below):
-
-| Word | Tools | Modules | Note |
-|---|---|---|---|
-| machine | 39 | 25 | the word most modules use for where they run |
-| node | 27 | 5 | 21 of the 27 in the agent's configuration module; the others the vault, the bus server, the packet filter and a flow editor whose own objects are called nodes |
-| the host / host's | 6 | 2 | the container module's `start` and `stop` tools, meaning the node-engine; the remaining uses are a network's hosts |
-| operator | 28 | 23 | the person; consistent with the glossary |
-| user | 30 | 12 | almost all a wrapped program's own accounts (an identity provider's users, the forge's users, a database's users) |
-| install | 43 | 16 | a package installed on a machine, never the mesh's assignment |
-| condition | 0 | 0 | — while the controller's verb is `conditions` |
-| seat | 0 | 0 | the module tools never name a seat; only the console and the controller do |
-| deploy, rollout, pipeline, control plane | 1, 0, 0, 0 | — | the one *deploy* is a wrapped program's own (a flow editor's) |
-
-The seat verbs say:
-
-- `/node-service-manager.start` and `.stop`: *"the answer says **the host** will restore what its
- declaration says at its next apply"*.
-- the bus server module's `connections` tool: *"a machine's **host** `node.`, a machine's runtime
- `.node-tools`"* — three words (host, node, runtime) for two programs, in one line.
-- `mesh-controller.plan`: *"What one machine would run, and why: **the declaration** the mesh would send
- it"*; `mesh-controller.plans`: *"What the last merges produced … the tiers"*;
- `mesh-controller.delivery-plan`: *"The delivery plan of a diffset"*. Three meanings of *plan* on one
- seat.
-- `mesh-controller.queue`, `.cancel`, `.clear`: *"Every **ask** in the build queue"* — while the glossary's
- *ask* is a request for the operator's input.
-- `mesh-delivery.release`: *"A person's word that a held delivery goes on"*; and
- `anthropic-licence-manager.release`: *"Unbind a consumer"*.
-- `mesh-controller.doctor`: *"The **self-check** …"* — the verb and the concept named differently.
-- the nftables module's tool is called `firewall_rules`, and the seat it holds on every machine is
- `node-packet-filter`, whose verb is `rules`.
-- `mesh-controller.nodes`: *"Every **machine** the mesh knows"*; the verb is named for nodes and answers
- in machines.
-- `node-build-agent` is the name of a seat every machine holds, while the glossary avoids *agent*
- *"because the word already means two other things here, the build agent and the operator's coding
- agent"*, and ADR 0237 calls the same role *the build seat*.
-- `operator-channel` is a seat of the mesh, held by the conversation's router (ADR 0234); the glossary
- names the router and the `channel` bench but not the seat.
-
-**Vendor words are not drift.** A module that wraps a program speaks that program's language where it
-describes the program's own objects: an identity provider has *users* and *realms*, a media manager has
-*releases*, a flow editor *deploys*. That is correct and must stay so; a check that flagged it would be
-wrong ([04](04-how-the-glossary-is-checked.md) scopes the list to the mesh's own words for that reason).
-The drift is where a tool describes **the mesh's** objects — its machines, its engine, its seats — in a
-word the glossary retired or never had.
diff --git a/01-RESEARCH/034-the-mesh-in-domains/02-the-domains.md b/01-RESEARCH/034-the-mesh-in-domains/02-the-domains.md
deleted file mode 100644
index 769f9d7a..00000000
--- a/01-RESEARCH/034-the-mesh-in-domains/02-the-domains.md
+++ /dev/null
@@ -1,292 +0,0 @@
-# 02 — The domains
-
-A proposed split of the mesh's concepts into domains, tested against the operator's starting guess and
-against the seven contexts of ADR 0006.
-
-## What a domain is here, and how one was drawn
-
-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. Another domain may use the word — that is what makes
-it a shared language — but may not change its meaning. Two relations between domains are named:
-
-- **upstream / downstream.** Domain A is *upstream* of B when B depends on A's concepts and A does not
- depend on B's. A change of meaning upstream ripples down; never the other way.
-- **shared word.** A word two domains both use. It is safe when both mean the same thing and one of
- them owns it; it is a clash when they mean different things ([03](03-the-clashes.md)).
-
-Three tests decided where a concept belongs, in this order:
-
-1. **Who records it.** The context, module or seat that writes the record of it owns it (ADR 0008: *a
- context owns its store*). A condition is the controller's record; a delivery is `mesh-delivery`'s.
-2. **Whose decision records define it.** The records that introduced the word, and the to-be design that
- names those records in its frontmatter.
-3. **Which other words it is never used without.** *Walk* never appears without *delivery*, *tier* and
- *gate*; *healer* never without *condition* and *budget*.
-
-**Domain and context.** ADR 0006's *context* is narrower than a domain: it is one owner of records, with
-one store, inside the controller. This effort proposes that a domain may hold zero, one or several
-contexts, and that the two words are kept apart: **domain** for the area of vocabulary and ownership,
-**context** for a store-owning part of the controller. Whether to adopt *domain* at all, or to widen
-*context* to mean it, is a question for graduation ([00](00-overview.md)); this document uses *domain*.
-
-## The split
-
-Ten domains of the mesh, and one of this repository. They are ordered from the most upstream to the most
-downstream; each lists its purpose, the concepts it owns with the proposed word for each, and its
-relations.
-
-### 1. Module — what a module is and declares
-
-**Purpose.** To say what one module is, in the one document every other domain reads: its manifest.
-
-**Owns.** *module*; *manifest* (the file a module is defined by — not *module definition*, not
-*module.yml* in prose); *resource* (what a manifest declares a machine must have: a file, a service, a
-container, a package); *declares* (the verb for what a manifest states); *provides* / *requires* (as
-written in a manifest; their meaning is Provisioning's); *claim* (a manifest's request to hold a seat);
-*setting* (a value a manifest declares and an assignment gives); *kept region*; *bundle* and *tools
-bundle*; *tool* (what a module serves) and *verb* (what a seat carries); *invokes*; *upgrade policy*
-(`roll`, `together`, `record`); the declarations of *data* and *health* (their meaning is Data's and
-Health's).
-
-**Relations.** The most upstream domain. Every other domain reads the manifest, which makes its words a
-**published language** — a vocabulary fixed in a schema that others conform to. It depends on nothing
-but the Core's notion of a scope.
-
-**Against the seven contexts.** None; the seven describe the controller, and the manifest is not the
-controller's.
-
-### 2. Core — the mesh's own machinery
-
-**Purpose.** To keep the controller, the bus, the store and every machine's engine running and agreed,
-so that every other domain has something to run on.
-
-**Owns.** *controller*; *control-node*; *node-engine*; *node tools* (the tool runtime on every machine);
-*foundation*; *store* (the one database server); *bus*; *broker seat*; *genesis* (raising the foundation
-on an empty mesh); *lease* and *epoch* (ADR 0229: which controller may send, and the order of what it
-sent); *context* (a part of the controller that owns a store).
-
-**Relations.** Upstream of every domain but Module. Every other domain is a **conformist** to it — it
-uses the bus and the store as they are and has no say in their shape.
-
-**Against the seven contexts.** Not one of them: ADR 0006 described what the controller owns, and the
-Core is the controller itself.
-
-### 3. Placement — what runs on which machine
-
-**Purpose.** To decide, send and apply what each machine runs, and to say why.
-
-**Owns.** *machine* (or *node* — see clash 1); *operator account*; *assignment* (a module put on a
-machine); *seat*, *scope* (machine, site, mesh), *capacity*, *bench*, *kind*, *holder*, *holding*;
-*depends on a seat*; *declaration* (what the controller sends one machine — see clash 9); *send* (the
-controller giving a machine its declaration; `push` is the verb that asks for it — clash 4); *apply* (the
-node-engine making a machine match its declaration); *reconcile*; *converged* and *adopted* (a machine's
-two states, ADR 0100); *pin* is Provisioning's, not this domain's.
-
-**Relations.** Downstream of Module and Core. Upstream of Provisioning, Connectivity, Data and Health,
-which all need to know what runs where. Shares *setting* with Module (a manifest declares it, an
-assignment gives it its value) and *declaration* with Module (clash 9).
-
-**Against the seven contexts.** ADR 0006's *inventory* (nodes, modules, assignments, versions) and the
-derivation half of *config*.
-
-The operator's guess called this *running modules on machines (seats, benches, claims)*, and it holds.
-*Claim* belongs to Module (a manifest claims) and *holding* to Placement (an assignment holds), which is
-the distinction the glossary already draws in *installed / holding*.
-
-### 4. Provisioning — one module serving another
-
-**Purpose.** To resolve which module serves a provision for which consumer, and to wire the two.
-
-**Owns.** *provision*; *provider*; *consumer* (the module that requires); *pin* (a person's choice of
-provider); *endpoint* (where a consumer reaches its provider); *pair credential* (the two ends of one
-credential; the credential itself is Identity's); *retire* (a provider stops serving a consumer the mesh
-no longer asks for, ADR 0230).
-
-**Relations.** Downstream of Module (requires/provides), Placement (what runs where) and Identity (the
-credential). Upstream of Data, which watches what a provider keeps for a consumer. Shares *consumer* with
-Identity's licences (a licence is bound to a consumer) — the same idea, safely shared.
-
-**Against the seven contexts.** ADR 0006's *provisioning*, unchanged.
-
-The operator's guess has no such domain; its words are spread across *running modules* and *data*. They
-are a language of their own: 95 decision records say *provider*, and four seat verbs (`pin`, `unpin`,
-`retire`, `cleanup`) are about nothing else.
-
-### 5. Identity and access — who may do what
-
-**Purpose.** To issue, hold, rotate and check the credentials and grants by which modules, machines,
-agents and the operator act.
-
-**Owns.** *credential* (anything that proves an identity); *secret* (a credential or other value the vault
-keeps sealed); *vault*; *grant* (what an identity may call or reach); *bus account* and *membership*; *rotate*;
-*licence*, *binding* (a consumer bound to a licence), *adopt* (taking in a licence that existed before);
-*proof* (what an authorising answer carries: a verified sender, a one-time code, a key's touch);
-*operator identity* (the list the router checks a sender against).
-
-**Relations.** Downstream of Core (the bus's accounts) and Placement (what is assigned where). Upstream of
-Provisioning, Operator and conversation, and Change and delivery (whose builds are signed and whose
-merges are approved).
-
-**Against the seven contexts.** ADR 0006's *identity*, plus the secrets half of *config*, plus the
-`licences` store, which is a context the seven did not name.
-
-The operator's guess has no such domain. It is not small: *credential* is in 78 decision records, *grant*
-in 65, *secret* in 56, and two of the three context stores that exist today (`identity`, `licences`) are
-its.
-
-### 6. Change and delivery — a commit on its way to the machines
-
-**Purpose.** To take one commit from its pull request to every machine that should run it, and to put it
-back when it fails.
-
-**Owns.** *pull request*, *commit*, *trunk*; *merge check* (the two statuses a pull request carries, the
-*merge gate* and the *repository check*, ADR 0238); *build*, *build seat*, *build queue* and the entry in
-it (clash 7); *package* and *package-registry*; *artifact* and *artifact store*; *catalogue* (what the mesh
-holds of every module, at which commit); *delivery*, *delivery group*, *delivery plan* (its *build plan*
-and *deploy plan*); *walk*; *tier* in the sense of a step of a walk (clash 8); *first machine* and its
-*gate* (clash 6); *rollback*; *the lab*, *scenario*, *replay*.
-
-**Relations.** Downstream of Module, Core, Health (the gate reads health) and Data (a module holding
-irreplaceable data is recorded, not rolled out, ADR 0236). Upstream of Placement in time — a walk asks
-the controller to send — and of Operator and conversation, which carries its held deliveries to a person.
-
-**Against the seven contexts.** ADR 0006's *delivery*, now owned by the `mesh-delivery` module rather than
-by a context of the controller (ADR 0239).
-
-The operator's guess called this *building and delivering changes*; it holds, with *change* kept for a
-diff (the glossary already says so) and *delivery* for the thing that travels.
-
-### 7. 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.
-
-**Owns.** *health* (a module's or core component's statement of being well, ADR 0240); *probe*;
-*signal* and *watchdog*; *self-check* (clash 13); *condition* (one open fact about something wrong, with a
-key and a severity); *healer*, its *repair* and *budget*; *drill*; *hand-act*; *observation* (ADR 0231:
-only observation raises or clears a condition).
-
-**Relations.** Downstream of everything it watches. Upstream of Change and delivery (the gate) and of
-Operator and conversation (a condition becomes a message).
-
-**Against the seven contexts.** ADR 0006's *observability* — whose row says *"health, logs, metrics,
-alerts"*. The mesh never built *alerts*; it built conditions, and the word *alert* survives only in a
-wrapped dashboard program's tool. The domain is renamed accordingly: *observability* names the watching,
-and half of what this domain owns is repair.
-
-The operator's guess called this *health and alerts*. It holds as a domain, under the word the mesh
-uses: *condition*, not *alert* (clash 14).
-
-### 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.
-
-**Owns.** *data* as a module declares it (ADR 0233); *data class* — *irreplaceable*, *rebuildable*,
-*cache*; *backup*; *restore point*; *restore* (beside the live data, never over it); the bus's *stream
-snapshot* (ADR 0235); *binding* of a consumer to its data moving only by a person (ADR 0232).
-
-**Relations.** Downstream of Module (the declaration), Placement (where the data is) and Provisioning
-(data a provider keeps for a consumer). Upstream of Change and delivery (irreplaceable data holds a
-module back from rolling out) and of Health (data is watched).
-
-**Against the seven contexts.** None. Data was not foreseen in ADR 0006; the self-check measures it today
-(`mesh-controller.data`), and every machine's `node-backup` seat keeps it.
-
-The operator's guess called this *data and backups*, and it holds.
-
-### 9. Connectivity — how machines and modules reach one another
-
-**Purpose.** To make every machine and module reachable by name where it should be, and unreachable
-where it should not.
-
-**Owns.** *private network* (clash 15); *resolver*; *uplink*; *hostname*; *zone*; *proxy* and *public
-name*; *packet filter* (clash 16); *intrusion prevention* and *ban*; *reach* (how far an
-assignment's endpoint may be reached from, ADR 0138); the roles a machine plays for the network (the anchor, a hub).
-
-**Relations.** Downstream of Placement (what runs where) and Identity (who may reach). Upstream of
-Provisioning (an endpoint must be reachable) and of Health (an unreachable machine is a condition).
-
-**Against the seven contexts.** ADR 0006's *connectivity*, unchanged — and the one domain research 005
-found real in the catalogue's history, where its modules are the only ones that change together.
-
-The operator's guess called this *the network*, and it holds.
-
-### 10. 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.
-
-**Owns.** *operator* (the person who runs the mesh; clash 17); *person* (any human, as against the mesh
-acting unattended); *console* (the person's or agent's end of node tools, clash 5); *channel* and *intake*
-benches; *router* and the `operator-channel` seat it holds; *responder*; *ask*, *authorising ask*,
-*answer*; *operator message* and *untrusted input*; *reference*; *notification* on a desktop.
-
-**Relations.** Downstream of Health (conditions become messages), Change and delivery (a held delivery
-waits for a person's word) and Identity (proofs, the operator's identities). An authorising answer acts in
-whichever domain asked, through that domain's own verb; the conversation owns the asking, never the act.
-
-**Against the seven contexts.** None. ADR 0006 named `api`, *"the one interface every surface speaks
-to"*, as an interface rather than a context; the console and the conversation are what it became.
-
-The operator's guess called this *people, conversation and approvals*. It holds, with one adjustment:
-an **approval** is not this domain's concept. The approval of a merge is Change and delivery's (a human
-approves the pull request); the release of a held delivery is `mesh-delivery`'s verb; the conversation
-carries both as asks. *Approval* is in five decision records; *ask* in 58.
-
-### 11. The record — how this repository works
-
-Not a domain of the mesh but of the company's way of building it, and listed because its words collide
-with the mesh's. **Owns** *research effort*, *decision record* (and *supersede*, *progressive insight*),
-*design* (*as-is*, *to-be*), *issue*, *playbook*, *check* (one of `00-META/checks/`), *graduation*,
-*hand-off*. Its *record* and *check* collide with the mesh's (clashes 11 and 12).
-
-## The operator's guess, scored
-
-| Guess | Verdict |
-|---|---|
-| building and delivering changes | **holds** as *Change and delivery* |
-| running modules on machines (seats, benches, claims) | **holds** as *Placement*, with *claim* moved to Module |
-| health and alerts | **holds, renamed** *Health and repair*: the mesh's word is condition, and half the domain is repair |
-| data and backups | **holds** as *Data* |
-| people, conversation and approvals | **holds** as *Operator and conversation*, with *approval* owned by the domain whose act is approved |
-| the network | **holds** as *Connectivity* (ADR 0006's word) |
-| — | **added**: *Module* (the published language), *Core* (what the rest runs on), *Provisioning* (ADR 0006 had it), *Identity and access* (ADR 0006 had it) |
-
-## A map
-
-```
- Module (published language)
- |
- Core
- |
- +--------------------------+---------------------------+
- | | |
- Identity -------------> Placement |
- | / | \ |
- | / | \ |
- v v v v |
- Provisioning <------- Connectivity Data |
- | | |
- +-------------> Health and repair <------------------+
- | |
- v v
- Change and delivery Operator and conversation
- | ^
- +-------------------+
- (a held delivery waits for a person's word)
-```
-
-Arrows point downstream. Change and delivery appears low because it reads health and data; in time it
-runs first, and asks Placement to send.
-
-## What this split does not decide
-
-- **Module ownership.** A module may serve several domains (the controller serves five). The domains
- are a partition of words and records, not of the catalogue — research 005 already found that grouping
- modules into folders records nothing.
-- **Whether every domain gets a context with its own store.** ADR 0008's rule applies where a domain's
- records live in the controller; *Change and delivery* has moved its records to a module instead, and
- that is equally sound.
-- **The operator's machine.** The desktop seats (display session, launcher, clipboard, notifier) are
- Placement's seats like any other, and their words are each program's own. Research 018 and 026 did not
- introduce a vocabulary that needs a domain.
diff --git a/01-RESEARCH/034-the-mesh-in-domains/03-the-clashes.md b/01-RESEARCH/034-the-mesh-in-domains/03-the-clashes.md
deleted file mode 100644
index e3d8c177..00000000
--- a/01-RESEARCH/034-the-mesh-in-domains/03-the-clashes.md
+++ /dev/null
@@ -1,306 +0,0 @@
-# 03 — The clashes
-
-Twenty places where the mesh's words disagree with each other. A **synonym clash** is two or more words
-for one concept; a **homonym clash** is one word for two or more concepts. Each entry gives the evidence
-— which documents and which tool descriptions say what — and a proposed single word, with the domain
-([02](02-the-domains.md)) that would own it. Counts are files containing the word, as in
-[01](01-the-concepts-in-use.md), unless they say *occurrences*.
-
-The first five are ordered by cost: how often the clash appears where somebody is told what to do (a
-to-be design, `00-META`, a tool description), and how likely it is to make a reader act on the wrong
-thing. The other fifteen follow by domain.
-
-A decision record keeps the words it was written with (its immutability rule), so a retired word found in
-a decision record is not a fault and is not counted as drift below unless the record was written after
-the word was retired.
-
-## The five that cost most
-
-### 1. *machine* and *node* — one concept, two words (synonym)
-
-**Evidence.**
-
-- The glossary defines **node**: *"a machine in the mesh"*, and has no entry for *machine*.
-- In the decisions and designs of the last week (ADRs 0220 to 0240, to-be 45 to 48), *machine* occurs 706
- times and *node* 349 times. Before ADR 0150, 6 record titles say *machine* and 9 say *node*; from ADR 0150
- on, 17 and 17.
-- In the tool descriptions, *machine* is in 39 tools of 25 modules; *node* in 27 tools of 5 modules, 21 of
- them one module's (the agent's configuration, where *node* is also a scope's value).
-- The console's own tools are `mesh_machine` and an overview that lists *"machines"*. The controller's verb
- is `nodes` and answers *"Every **machine** the mesh knows"*; `node` answers *"What one machine
- reported"*. Every seat a machine holds is named `node-…`.
-- The glossary itself switches inside one entry: *control-node* is *"the one node that also holds…"*, and
- the next sentence says it *"is not a separate kind of machine"*.
-
-**Proposed word: _machine_** (Placement). It is what the operator, the tools and the newest records already
-say, and it needs no explaining. *Node* survives only as an **identifier**: the value of a scope
-(`scope: node`), the `node-` prefix of seat names, and *control-node* until it is renamed. The glossary
-would map the identifier to the word, as it does for `mesh-host` today. The alternative — *node* everywhere —
-is defensible and cheaper in code, and is what the glossary says; it is listed in
-[00](00-overview.md) as the operator's call because either choice renames something heavily used.
-
-### 2. *node-engine*, *mesh-host*, *the host*, *node host*, *host agent* — one program (synonym)
-
-**Evidence.**
-
-- The glossary retired *host agent*, *the host* and `mesh-host` for **node-engine** on 2026-10-05, keeping
- `mesh-host` as the name of the repository, binary and service unit until they are renamed.
-- Its own node tools entry still says *"a host-side process **the host** supervises"*.
-- To-be 05 is titled *the node host*; *the host* is in 34 to-be designs and *node-engine* in 7.
-- Of the decision records written since the retirement, ADR 0222 says *the host* six times, ADR 0236 six
- times beside *node-engine* nine times, and ADR 0223 three of each.
-- Tool descriptions: `/node-service-manager.start` and `.stop`, and the container module's
- `start` and `stop`, say *"the answer says **the host** will restore …"*. The bus server's `connections`
- tool says *"a machine's **host** `node.`"* — naming the program *host* and its bus account
- *node*.
-- *Host* also keeps its ordinary meanings: a network host (the ssh client module's tools), *hosting* a
- service, and the *host's* network in a container runtime.
-
-**Proposed word: _node-engine_** (Core), as the glossary already says; *the host* and *host agent* retired
-for the program. Because *host* also has ordinary networking meanings, the check in
-[04](04-how-the-glossary-is-checked.md) can retire only the phrases *the host* and *host agent* and the
-identifier `mesh-host` outside code spans, not the bare word. If clash 1 is decided for *machine*, the
-program's name reads more naturally as **machine engine**; this effort does not propose renaming it again.
-
-### 3. *plan* — four things (homonym), and *change plan* / *release plan* / *delivery plan* (synonyms)
-
-**Evidence.** On one seat, the controller's:
-
-- `mesh-controller.plan` — *"What one machine would run, and why: the declaration the mesh would send it"*.
- A **machine's declaration**, previewed.
-- `mesh-controller.plans` — *"What the last merges produced and where each stands (ADR 0162): the
- tiers"*. ADR 0162 calls it *a tiered plan*; ADR 0218 and ADR 0236 call it the **release plan** (seven
- times in ADR 0236: *"Every release plan's first machine passes a gate"*).
-- `mesh-controller.delivery-plan` — *"The delivery plan of a diffset"*. The glossary: *"ADR 0238 called it the
- **change plan**; that word is retired"* — and the walk replaces the release plan: *"the controller's
- plan is now the walk of one delivery's trunk commit across machines"*.
-- `mesh-controller.bus` — *"The bus as a **planned step**"*.
-
-*Release plan* appears in seven files created on or after 2026-10-06, the day ADR 0239 replaced it; *change
-plan* in four, including the title of ADR 0238.
-
-**Proposed words** (Change and delivery, except the first):
-
-| Concept | Word | Retire |
-|---|---|---|
-| what the controller would send one machine | **declaration** (Placement); the verb `plan` becomes `declaration` or `preview` | *plan* for this |
-| what a delivery would do to the mesh | **delivery plan** | *change plan*, *release plan* |
-| the controller's sending of one commit's builds across machines | **walk** | *release plan*, *plan* for this |
-| a step a person starts | **planned step**, or simply *step* | — |
-
-Bare *plan* is then never a mesh concept on its own.
-
-### 4. *push*, *send*, *roll out*, *release*, *deploy*, *upgrade* — moving a change (synonyms and homonyms)
-
-**Evidence.**
-
-- **push.** `mesh-controller.push`: *"Send one machine everything it should be. With no machine named it
- is a push of the WHOLE mesh"*. ADR 0221's title: *"a push sends no build a policy or a plan holds
- back"*. The same word is a git push (which, per the conventions, starts the build) and a phone
- notification (*"Matrix once its push is measured"*, to-be 46). *Push* is in 84 decision records and
- 152 issues.
-- **send.** ADR 0236: *"that machine is sent it, by the ordinary send"*; the gate is judged *"from the
- moment it was sent the build"*. The controller's own description of `push` uses *send*.
-- **roll out.** ADR 0236's title says *rolls out unattended*; its person-facing verb is `upgrade
- roll-out`, and the manifest's policy for the same thing is spelt `roll`. ADR 0218: *"code rolls out one
- machine first"*. *Roll out* is in 30 decision records and 36 issues.
-- **release.** `mesh-delivery.release` — *"A person's word that a held delivery goes on"*;
- `anthropic-licence-manager.release` — *"Unbind a consumer"*; and the retired *release plan* (clash 3).
-- **deploy.** The glossary: a delivery is *"not 'deployment' (one stage of it)"*; its *delivery plan* holds
- a *deploy plan*. The controller has no verb named deploy, and the only tool that says *deploy* is a
- wrapped flow editor's.
-- **upgrade.** `mesh-controller.upgrade` — *"How each module's new builds reach its machines … rolled out
- one machine first"*: a **policy**, not an act.
-
-**Proposed words:**
-
-| Concept | Word | Domain |
-|---|---|---|
-| the controller giving one machine its declaration | **send** (the verb `push` asks for one) | Placement |
-| one commit travelling to every machine | **delivery** | Change and delivery |
-| the per-machine part of a delivery plan | **deploy plan** (kept; the only use of *deploy*) | Change and delivery |
-| a build going to its first machine, judged, then the rest | **walk** (the act); **roll** (the policy, one spelling in the manifest and in the verb) | Change and delivery |
-| a person letting a held delivery go on | **release** — this sense only | Change and delivery |
-| unbinding a licence's consumer | **unbind** (rename the licence verb) | Identity |
-| how a module's builds reach its machines | **upgrade policy** | Module |
-
-*Push* is kept for the controller's verb because it names what the operator does, but prose says *send*;
-*roll out* becomes the policy's value `roll` and is not a noun.
-
-### 5. *console*, *node tools*, *mesh-console*, *the MCP server* — the operator's surface (synonym and homonym)
-
-**Evidence.**
-
-- The glossary's surfaces section: the console is *"the module (`mesh-console`)"*; not *"the tool bridge",
- "the brain" or "the MCP server"*.
-- The glossary's operator's machine section: node tools *"replaces 'console' as the module's name;
- console remains the word for the person's end of it"*. Both entries are current.
-- The live console introduces itself as *"The tools of a Novox mesh, reached as `.node-tools`"*,
- under an MCP server named `mesh`; the organisation's instructions to the agent call that server *"the
- console"*.
-- *Console* is in 28 decision records; `mesh-console` in 1; *node tools* in 10. A wrapped chat program's
- tool and the localisation module's tool use *console* in their own senses (a virtual terminal, a web
- console).
-
-**Proposed words:** **node tools** (Core) for the runtime that serves every module's tools and every seat's
-verbs on a machine; **console** (Operator and conversation) for the end of it a person or agent uses — the
-MCP endpoint and its five tools. `mesh-console` retired. The glossary's surfaces entry is rewritten at
-graduation; the contradiction is removed, not annotated.
-
-## Change and delivery
-
-### 6. *gate* — three things (homonym)
-
-ADR 0136 (*a step gates its module, not the machine*): a failing step of a module's apply stops that
-module's later steps. ADR 0236: the **gate** on a delivery's first machine — *"healthy three times"*.
-ADR 0237 and ADR 0238: the **merge gate**, a status a pull request carries (`mesh/merge-gate`). The
-controller's description of `upgrade` says *"judged there at the gate"*; a wrapped forge's tools say
-*branch protection*. **Proposed:** *first-machine gate* for ADR 0236's, *merge gate* for the status, and
-ADR 0136's becomes *a failed step holds its module* (no noun). Bare *the gate* is not used.
-
-### 7. *ask* — the operator's question, and an entry in the build queue (homonym)
-
-The glossary: *"**ask** — a request for the operator's input, of a declared kind"* (ADR 0234).
-`mesh-controller.queue`: *"Every **ask** in the build queue: waiting, in flight … and dead"*;
-`.cancel`: *"Drop one waiting or dead ask"*; `.clear`: *"Cancel every waiting ask"*. ADR 0234's ask is in
-58 decision records' worth of prose shared with the ordinary verb *to ask*. **Proposed:** *ask* stays the
-operator's (Operator and conversation); an entry in the build queue is a **build request** (Change and
-delivery), and the three verbs' descriptions say so.
-
-### 8. *tier* — three things (homonym)
-
-The `topic: the tiers` of 36 decision records, and `repos.md`'s *Tier 0 … 3*: the **layers** of the mesh
-(node-engine, foundation, controller, surfaces). ADR 0162 and to-be 47: the **steps of a walk**, a module
-and the modules built against it (*"its builds are asked and registered tier by tier"*). To-be 46: the
-**levels of authority** of an ask (*"a kind below the tier"*, *"no P2 tier before recovery codes"*).
-**Proposed:** *layer* for the first (Core); *tier* for the walk's steps (Change and delivery), which is how
-the tools use it; *assurance level* for the third (Identity and access).
-
-### 9. *declaration* — what a manifest states, and what the controller sends (homonym)
-
-The glossary: the node-engine *"receives the node's **declaration**"* — the controller's per-machine
-document. The glossary again: a module *"**declares** resources"*, and *depends on a seat* is *"derived from
-the resources it declares"* — the manifest. `mesh-controller.plan` returns *"the declaration the mesh would
-send"*; the container and service-manager tools say *"what its declaration says"*. *Declaration* is in 106
-decision records, in both senses. **Proposed:** a manifest **declares** (Module; the noun is *the
-manifest*), and what the controller sends one machine is that machine's **declaration** (Placement). The
-verb and the noun then never name the same thing, and *declaration* is not used for a manifest.
-
-### 10. *control plane* and *controller* (synonym, drift)
-
-Retired by the glossary on 2026-09-16. Since then: 29 occurrences in 12 to-be designs (`05`, `07`, `08`,
-`18`, `19`, `26`, `28`, `29`, `30`, `32`, `33`, `01`) and one as-is design; 73 files created after the
-retirement use it — 36 issue reports, 29 decision records, 6 designs — among them issue 156, titled *moving a consumer's delivery subject
-stops the control plane*. ADR 0006, the record the word came from, still has it in its
-title. **Proposed:** *controller* (Core), as decided; the to-be designs are corrected at graduation, which
-is a link-and-word fix the README's rules allow.
-
-## The record and its checks
-
-### 11. *record* — four things (homonym)
-
-A **decision record** (this repository). **The record** of the mesh — the `records` module that agents
-search first (*"search the record with the literal text"*), and ADR 0006's *"where the record lives is
-deliberately open"*. An **upgrade policy** value: `record` means *register the build, send it nowhere*
-(ADR 0236). **On record**: a seat holder the controller has written down (ADR 0223's *"each on record"*).
-**Proposed:** *decision record* always qualified in hq; *the record* for the mesh's knowledge base (Operator
-and conversation, or a domain of its own if the records module grows one); the policy value renamed
-**hold** — which is also what *held* means for a delivery — and *on record* left as ordinary English.
-
-### 12. *check* — at least six things (homonym)
-
-The checks in `00-META/checks/`; a pull request's **merge check**, made of the **merge gate** and the
-**repository check** (`mesh/repo-check`, ADR 0238); a delivery group's **composed check**
-(`mesh-controller.delivery-check`); the state `checked` of a delivery; the **self-check** (clash 13); a
-module's tool named `…_check` (the ssh client's). *Check* is in 92 decision records. **Proposed:** never
-bare in a governing document; always one of *merge check*, *repository check*, *composed check*,
-*self-check*, *hq check*. The word is too ordinary to retire, so this one is a writing rule, not a list
-entry ([04](04-how-the-glossary-is-checked.md) says why it cannot be checked mechanically).
-
-## Health and repair
-
-### 13. *self-check* and *doctor* (synonym)
-
-To-be 45 names the concept **self-check** (8 occurrences) and the verb `doctor` (14); the verb's own
-description begins *"The self-check (to-be 45 §4)"*. **Proposed:** *self-check* (Health and repair); the
-verb renamed `self-check`, or kept as `doctor` with the glossary mapping it — a verb name is an identifier.
-
-### 14. *condition*, *alert*, *problem*, *incident*, *finding* (synonyms)
-
-The controller's verb `conditions`: *"What is wrong with the mesh now: every open condition"*; to-be 45 says
-*condition* 72 times. ADR 0006's observability context owns *"health, logs, metrics, **alerts**"*. ADR
-0224's title: *"a provider that keeps failing a consumer is a **problem** the controller reports"*. The
-container module's tool is `docker_problems`. *Incident* is in 14 decision records, mostly as *the incident
-that earned a rule*. *Finding* is in 19, as what a check or a probe found. **Proposed:** *condition*
-(Health and repair) for an open fact about something wrong; *finding* for what one probe or check returned
-before it becomes a condition; *incident* kept for a past event that taught a rule (The record); *alert* and
-*problem* retired for the mesh's own notion. A wrapped dashboard's *alerts* are its own.
-
-## Connectivity
-
-### 15. *private network* and *overlay* (synonym)
-
-The operator, quoted in ADR 0226 (2026-10-06): *"we simply have a private network"*; ADR 0226's title and
-the `mesh-wireguard` module's role: *the private network*, in 37 decision records. To-be 08 (connectivity)
-says *overlay* 24 times; *overlay* is in 30 decision records and 13 to-be designs. No tool says either.
-**Proposed:** *private network* (Connectivity), the operator's word.
-
-### 16. *packet filter* and *firewall* (synonym)
-
-The seat every machine holds is `node-packet-filter`, with the verb `rules` (*"The packet filter as this
-machine enforces it now"*). The nftables module's tool is `firewall_rules`. *Firewall* is in 33 decision
-records and 13 to-be designs; *packet filter* in 22 and 12. **Proposed:** *packet filter* (Connectivity),
-after the seat; the module's tool renamed `packet_filter_rules` or described in the seat's word.
-
-## Operator and conversation
-
-### 17. *operator*, *the user*, *person* (synonym, partly)
-
-The glossary's *operator account* entry: *"Not 'the user' (ambiguous with a module's own account)"*. *The
-user* is in 10 to-be designs, several meaning a bus account (*"the user list"*, the bus server's word), and
-in 9 decision records. *Operator* is in 125 decision records and the tools of 23 modules; *person* in 91
-records, meaning a human as against the mesh acting unattended (*"a person's word"*). **Proposed:**
-*operator* for the person who runs the mesh; *person* for any human acting where the mesh could have; *the
-user* retired in the mesh's own prose; a bus account is a *bus account* (Identity). A wrapped program's
-*users* are its own.
-
-### 18. *agent* — three things (homonym)
-
-The glossary avoids *agent* for the node-engine because it *"already means two other things here, the build
-agent and the operator's coding agent"*. The seat every machine holds is `node-build-agent`, held by the
-`build-agent` module; ADR 0237 calls the role *the build seat*, ADR 0236 *the build machine* (14 decision
-records). The licence manager's verbs say *"a node's **agent**"* for the coding agent a licence is bound to.
-A wrapped memory server has *agents* of its own. **Proposed:** *agent* only for a coding agent (the
-operator's, or one the mesh runs); the build role is the **build seat**, its holder on a machine the
-**builder** — and the seat renamed when seats are next renamed. *Build machine* retired: any machine may
-hold the seat.
-
-## Core
-
-### 19. *store* — four things (homonym)
-
-The glossary: **store** — *"the one postgres server"*. ADR 0189's title, *"The store keeps what the
-records name"*, is about the **artifact store**. Research 015 is about the **object store**. ADR 0006: *"a
-node reconciles from its own store"* — the machine's local copy of its declaration. **Proposed:** *store*
-alone means only the foundation's database server (Core); the others are always *artifact store*, *object
-store*, and the machine's *last declaration*.
-
-### 20. The glossary's own contradictions
-
-Recorded in [01](01-the-concepts-in-use.md) and listed here because the fix is the same kind: the console
-(clash 5); the deprecated broker, which has no seat in its own entry and claims `mesh-broker` in the entry
-for *claim*; and node tools described with the retired *the host*. **Proposed:** fixed at graduation,
-in the same change that reorganises the glossary ([05](05-a-glossary-by-domain.md)).
-
-## Checked and found consistent
-
-Worth saying, so nobody repeats the search:
-
-- **catalogue / catalog.** Prose says *catalogue* everywhere (no bare *catalog* in any decision, design or
- `00-META` file); *catalog* appears only inside identifiers (`mesh-catalog`, `catalog_modules`). That is an
- identifier, not a clash.
-- **foundation / substrate.** Of the 10 files created since *substrate* was retired that still use it, 9
- are decision records and research (allowed to keep their words); the tenth is the glossary, naming it
- as retired.
-- **flavor.** Retired; two to-be designs (08, 16) still say it, both predating the retirement.
-- **operator** in the tools is used consistently for the person, in 28 tools of 23 modules.
diff --git a/01-RESEARCH/034-the-mesh-in-domains/04-how-the-glossary-is-checked.md b/01-RESEARCH/034-the-mesh-in-domains/04-how-the-glossary-is-checked.md
deleted file mode 100644
index 9da8bbd8..00000000
--- a/01-RESEARCH/034-the-mesh-in-domains/04-how-the-glossary-is-checked.md
+++ /dev/null
@@ -1,140 +0,0 @@
-# 04 — How the glossary is checked
-
-[`AGENTS.md`](../../AGENTS.md): *"If a document states a rule about the mesh, it says how that rule is
-verified. An unenforced rule is indistinguishable from a wrong one."* The glossary states two rules — *one
-name per thing*, and *"a new name for an existing thing lands here first, in the same change that
-introduces it in code"* — and nothing verifies either. [03](03-the-clashes.md) is what that costs. This
-document describes a check; it does not build one. Building it follows graduation.
-
-## What a word list can and cannot catch
-
-A list of retired words catches **synonyms**: a word the glossary replaced, used again. It cannot catch
-**homonyms** — *plan*, *gate*, *tier*, *ask*, *store*, *record*, *check* — because the word is right in one
-sense and wrong in another, and telling them apart is reading, not matching. So the proposal has two
-parts: a mechanical check for synonyms, and a rule for homonyms that turns each one, once decided, into a
-synonym the list can hold (*release plan* → *walk*; *build machine* → *build seat*; `ask` in the build
-queue's verbs → *build request*). A homonym left undecided stays a reviewer's job, and the glossary says
-so next to the word.
-
-## Part 1 — the list, kept in the glossary
-
-The list of retired words lives **in the glossary, in one fixed form**, so there is one source and no
-second file to drift from it. Every entry that retires words ends with one line:
-
-> *Not:* ~~control plane~~, ~~substrate~~
-
-and nothing else in the glossary is struck through. Today the glossary marks retired words three ways
-(struck through, *"Replaces x"*, *"Not x"*); graduation moves all of them to this line.
-
-Each struck word may carry a **scope**, in brackets after it, saying where it is retired:
-
-- *(hq)* — in this repository's governing documents only;
-- *(tools)* — in the descriptions of the mesh's tools and seat verbs only;
-- no scope — both.
-
-The scope exists because some words are wrong in one place and right in the other: *the user* is retired
-in hq prose, but a wrapped identity provider's tool must say *user*.
-
-An entry may also carry an **identifier** line naming code that still has the old name, and the issue that
-tracks its rename:
-
-> *Identifier until renamed:* `mesh-host` — the repository, binary and service unit (issue NNN)
-
-An identifier is allowed inside a code span (`` `mesh-host` ``) and nowhere else; when the issue resolves,
-the line goes and so does the allowance.
-
-## Part 2 — `00-META/checks/words.py`, over this repository
-
-**Reads** the glossary's *Not:* lines and *Identifier* lines. **Scans** the documents that tell somebody
-what to do — the same set `records.py` calls *governing*: `00-META/` (the glossary's own *Not:* lines
-excluded), `03-DESIGN/01-to-be/`, `AGENTS.md` and `README.md` — plus `03-DESIGN/00-as-is/`, whose words
-should be today's even where its facts are yesterday's.
-
-**Fails** on a retired word (of scope *hq* or none) in running prose. It **ignores**:
-
-- code spans and code blocks, which hold identifiers;
-- block quotes and text in quotation marks, which quote somebody — the operator, a tool, an older
- record — and must quote them faithfully;
-- link targets, which are file names, and file names are immutable when they are a decision record's;
-- `02-DECISIONS/` entirely, and `01-RESEARCH/` and `04-ISSUES/` written before the check's start date
- — records keep their words, and research and issues observe.
-
-**For research and issues written after the start date** the check applies the same rule. An issue report
-quoting a log or a tool keeps the quotation; its own prose uses today's words. This is a choice for
-graduation, listed in [00](00-overview.md); the reason to include them is that issue reports are the
-largest single source of drift found (36 of the 73 files that say *control plane* since its retirement).
-
-**The reverse rule.** A to-be design that defines a word in the glossary's form — a bolded word followed by
-a dash at the start of a definition — must find that word as a head word in the glossary. This is how
-*"a new name lands here first"* gets checked, and it would have caught *condition*, *healer* and
-*data class* arriving in to-be 43 and 45 with no entry.
-
-**The uniqueness rule.** No head word appears in two entries, and no head word is also struck through in
-another entry. This would have caught *release*: today a verb of `mesh-delivery`, and the retired *release
-plan*.
-
-**What it would fail on today** — the check is proposed because these are real, and per
-[`00-META/checks/README.md`](../../00-META/checks/README.md) a check must fail on something real before
-it passes:
-
-| Retired word | Where, today |
-|---|---|
-| control plane | 29 occurrences in 12 to-be designs; one as-is design |
-| the host (for the node-engine) | 34 to-be designs, and the glossary's own node tools entry |
-| release plan, change plan | to-be 45, 47 |
-| flavor | to-be 08, 16 |
-| the user | 10 to-be designs (several are a bus account) |
-
-Each is either fixed at graduation or, where it is a quotation, put in quotation marks — which is the
-check doing its job, making a quotation look like one.
-
-## Part 3 — the same list, over the catalogue's tool descriptions
-
-An agent meets the mesh's words most often in tool descriptions: 535 module tools and the verbs of 20 seats
-today. Nothing compares them with the glossary, and four of them say *the host*.
-
-**Where it runs.** In the catalogue repository's own repository check (`mesh/repo-check`, ADR 0238), which
-already reads every changed manifest. A changed manifest's tool descriptions and its seat's verb
-descriptions are matched against the *Not:* words of scope *tools* or none. A finding fails the check, as
-everything does on the mesh's repositories (a warning blocks a merge the same as a failure there, [issue
-293](../../04-ISSUES/293-a-warning-on-a-required-check-blocked-the-merge/00-report.md), so a warning-only mode
-is not available and is not proposed).
-
-**Where the list comes from.** The catalogue cannot read this repository at build time without a new
-dependency. Two options, for graduation:
-
-1. **A copy, checked.** The catalogue keeps a copy of the list, and `words.py` here fails when the copy in
- the catalogue's trunk differs from the glossary. Simple; two repositories change for one word, which is
- honest about what a retired word costs.
-2. **The list as an artifact.** This repository's merge publishes the list to the artifact store, and the
- catalogue's check reads the newest. One source; one more thing that must be up for a catalogue merge.
-
-The first is recommended: it fails loudly in the right place, and adds no runtime dependency.
-
-**Vendor words.** A module wrapping a program describes that program's objects in its own words — an
-identity provider's *users*, a media manager's *releases*, a flow editor's *deploy*. The list holds only
-the mesh's own retired words, and a word that is also a common vendor word (*user*, *release*, *deploy*)
-is never retired with scope *tools*. That keeps the check from crying wolf, which
-`00-META/checks/README.md` names as the way a check gets suppressed.
-
-**On the running mesh, later.** The descriptions an agent actually sees are the ones served, which may lag
-the catalogue's trunk. A probe in the self-check could compare the controller's `tools` verb with the same
-list and raise a condition. That is a second place for the same rule and is not proposed until the first
-has run.
-
-## Part 4 — homonyms, by rule
-
-For a word the glossary marks *homonym* (*plan*, *gate*, *tier*, *store*, *record*, *check*, *ask*,
-*agent*), the entry lists each sense with its qualified form and its domain, and the writing rule is: **a
-homonym is never bare in a governing document; it is always the qualified form.** This is not
-machine-checked — a word list cannot tell *the gate* that means a first-machine gate from one that means
-the merge gate — and the glossary says so next to the rule, as AGENTS.md requires: *"checked by review;
-the reviewer looks for the bare word in the diff."* Where a homonym is resolved by renaming one sense, the
-old sense moves to the *Not:* line and becomes mechanical.
-
-## What it costs
-
-- One Python file of the size of `cycle.py`, run by `merge-check.sh`.
-- A one-time correction of the governing documents it fails on, made in the graduation change.
-- A copy of the list in the catalogue, and a few lines in its repository check.
-- One rule for writers: a retired word may be quoted, never used.
diff --git a/01-RESEARCH/034-the-mesh-in-domains/05-a-glossary-by-domain.md b/01-RESEARCH/034-the-mesh-in-domains/05-a-glossary-by-domain.md
deleted file mode 100644
index 3038917e..00000000
--- a/01-RESEARCH/034-the-mesh-in-domains/05-a-glossary-by-domain.md
+++ /dev/null
@@ -1,101 +0,0 @@
-# 05 — A glossary by domain
-
-How [`00-META/glossary.md`](../../00-META/glossary.md) would be organised once the domains in
-[02](02-the-domains.md) are decided. Described only: the glossary changes in the graduation change, with
-the decision record that adopts the domains, and not before.
-
-## What is wrong with its shape today
-
-- **Its sections are a history, not a map.** *The operator's machine* holds node tools, bundle, kept region
- and installed / holding because they arrived with research 018, not because they belong together; node
- tools is a Core concept and kept region a Module one.
-- **It has no home for a new word.** A writer adding *condition* would not know which section it goes in,
- and so it went in none.
-- **Retired words are marked three ways**, so no program can read them ([04](04-how-the-glossary-is-checked.md)).
-- **It is one page for 33 entries**; with the roughly 50 words [01](01-the-concepts-in-use.md) found
- missing, it is about 85, and one flat list of 85 is read by nobody.
-
-## The proposed shape
-
-One file, as now — it stays the single authority, and a reader searches one page. Its sections are the
-domains, in the order of [02](02-the-domains.md) (most upstream first), preceded by two short sections.
-
-### 1. How to read this page
-
-What a domain is, in two sentences; that a word is defined once, in the domain that owns it; that other
-domains may use it but not change it; and the forms of the *Not:* and *Identifier* lines the check reads.
-
-### 2. Words every domain uses
-
-A short **shared kernel** — the handful of words with one meaning everywhere, owned by no single domain
-and changed only by a decision record: *mesh*, *machine* (or *node*, clash 1), *module*, *seat*, *operator*,
-*person*. Each points to the domain where its detail lives. Kept deliberately short: a word that only one
-domain refines belongs to that domain.
-
-### 3. One section per domain
-
-Each section opens with a fixed header, then its entries:
-
-- **Purpose** — one sentence, from [02](02-the-domains.md).
-- **Owned by** — the context, module or seat that records its concepts (*the controller's `inventory`
- context*; *the `mesh-delivery` module*; *every machine's `node-backup` seat*).
-- **Upstream of / downstream of** — the domains, by name.
-- **Decided by** — the decision records that define the domain's words, so a reader can go from a word
- to why.
-
-Each **entry** keeps today's prose form and gains at most three fixed lines:
-
-- the definition, in plain sentences, with its record cited (as now);
-- *Not:* ~~retired word~~, ~~another~~ *(scope)* — the words it replaced;
-- *Identifier until renamed:* `old-name` — what code still carries the old name, and the issue that
- tracks the rename.
-
-A word a domain uses but does not own is not repeated there; the domain's section may list it under *Uses*
-with a link, so a reader of one section sees its whole vocabulary without the definition being copied.
-
-### 4. Homonyms
-
-One section at the end for the words that mean different things in different domains — *plan*, *gate*,
-*tier*, *store*, *record*, *check*, *ask*, *agent*, *release*. For each, a small table: the sense, its
-qualified form, the domain that owns it. This is the page a reader lands on from a search for a bare
-word, and it says the rule beside the table: never bare in a governing document; checked by review.
-
-### 5. How this page is kept
-
-Today's section, extended: a new word lands here first, in the domain that owns it; a word moves domain
-only with a decision record; a retired word goes on a *Not:* line and nowhere else; and `words.py`
-checks the three rules it can ([04](04-how-the-glossary-is-checked.md)).
-
-## Where today's entries would go
-
-| Today's entry | Domain |
-|---|---|
-| node, control-node, operator account | shared kernel (machine); Placement (operator account); Core (control-node) |
-| controller, mesh-controller, node-engine, foundation, store, bus, the deprecated broker | Core |
-| package, artifact | Change and delivery |
-| seat, depends on a seat, installed / holding | Placement |
-| claim, provision (as declared), invokes, bundle, kept region | Module |
-| provision (as resolved) | Provisioning |
-| channel / intake, ask, operator message / input, reference, console | Operator and conversation |
-| node tools | Core |
-| delivery, delivery group, delivery plan, mesh-delivery, walk | Change and delivery |
-| ~~master / slave~~, ~~hub / peer~~, ~~flavor~~ | *Not:* lines on *controller* and *setting* |
-
-And the sections that are empty today and would be filled from the records that already define their
-words: **Identity and access** (credential, secret, vault, grant, bus account, licence, binding, proof —
-ADRs 0143, 0183, 0225, 0234); **Health and repair** (condition, probe, signal, watchdog, self-check, healer,
-drill, hand-act — to-be 45, ADRs 0227, 0231, 0240); **Data** (data class, backup, restore point, stream
-snapshot — to-be 43, ADRs 0233, 0235); **Connectivity** (private network, resolver, uplink, packet
-filter, reach — ADRs 0117, 0138, 0223, 0226).
-
-## What derives from it
-
-- **The decision index.** The records' `topic:` (*the mesh*, *the tiers*, *what runs on it*, *building
- it*, *checking it*, *how we work*) predates the domains and partly overlaps them. Whether a record also
- names its domain — a new frontmatter field, and so a schema change — is left for graduation. It would let
- `words.py` scope a word by domain, and `index.py` print the reading order per domain.
-- **The constitution page.** The mesh injects a page derived from `how-we-build.md` into design sessions
- (playbook 05). A derived glossary page per domain could be injected the same way, so an agent starts
- with the words; that is a separate decision, not proposed here.
-- **Tool descriptions.** Once a domain owns a word, a module describing that concept in a tool uses it; the
- check in [04](04-how-the-glossary-is-checked.md) holds them to the retired half, and review to the rest.
diff --git a/01-RESEARCH/035-the-images-from-before-the-mesh/00-overview.md b/01-RESEARCH/035-the-images-from-before-the-mesh/00-overview.md
deleted file mode 100644
index ee180334..00000000
--- a/01-RESEARCH/035-the-images-from-before-the-mesh/00-overview.md
+++ /dev/null
@@ -1,85 +0,0 @@
----
-status: active
-initiated: 2026-10-08
-touches:
- - 02-DECISIONS/0189-the-store-keeps-what-the-records-name.md
- - 02-DECISIONS/0251-the-registries-say-what-they-hold-and-keep-what-is-named-and-a-machine-keeps-the-image-it-runs-and-the-one-before.md
- - 03-DESIGN/01-to-be/51-the-registries-keep-what-is-named.md
- - the artifact store, the catalogue's modules that pin an image by digest
----
-
-# 035 — The images from before the mesh
-
-## What is investigated
-
-The artifact store's tools ([ADR 0251](../../02-DECISIONS/0251-the-registries-say-what-they-hold-and-keep-what-is-named-and-a-machine-keeps-the-image-it-runs-and-the-one-before.md))
-counted 469 unrecorded manifests marking 23.8 GiB on 2026-10-08. 374 of them are copied bases
-([issue 321](../../04-ISSUES/321-a-copied-base-was-recorded-by-nothing-and-a-let-go-index-kept-its-platforms/00-report.md),
-[ADR 0257](../../02-DECISIONS/0257-a-copied-base-is-recorded-where-it-is-copied-kept-by-the-builds-that-stood-on-it-and-held-once-per-image.md)).
-Most of the rest are images pushed to the same store before the mesh existed. The predecessor's
-pipeline built them, under a namespace per owner, one repository per app.
-
-No record of the mesh names these images, so [ADR 0189](../../02-DECISIONS/0189-the-store-keeps-what-the-records-name.md)
-§3 keeps every tool away from them. That is correct: the mesh did not make them. It also means they stay
-until a person decides. This effort finds out, for each one, whether anything still uses it, and
-recommends what to do. Nothing was deleted.
-
-## How it was measured
-
-All of this was read, nothing was written. On 2026-10-08:
-
-- **The store's view.** `artifact_store_references` and `artifact_store_repositories` give each
- repository's manifests, tags and sizes. They give no dates.
-- **What each machine declares.** The controller's `images` verb answers for each of the four machines.
-- **What each machine runs.** The container runtime module's container list answers on each machine.
-- **What still names them.** I searched every repository on the forge for each image's path and digest.
- There are 50 repositories, and I read 31 of them closely: both catalogues, the apps' own repositories,
- the setup repositories and the predecessor's.
-- **Dates.** No tool gives a manifest's push time. A date below is either an image's `created` date, from
- a copy still in a machine's local image store and matched by tag, or the date of the commit the tag
- names. Where neither exists, the date is not known, and the table says so.
-
-## What was found
-
-[01-the-images.md](01-the-images.md) has the table. In summary:
-
-- **3 repositories hold an image a machine runs today.** Each is one of three app images on the anchor:
- an invoicing app's API, its web front, and a static company website. A catalogue module pins each by
- digest, under an older name of the same store. Each repository also holds two older tags that
- nothing uses.
-- **2 repositories are pinned only by catalogue modules that no machine is assigned.** One is a
- magazine site's older image. The other is an older photo client, pinned by two per-person photo
- modules. Both are stale definitions.
-- **35 repositories, plus 2 single manifests in repositories the mesh builds, are used by nothing.**
- Nothing runs them, declares them or pins them.
- - Most were superseded by a module that builds its own image of the same app.
- - The rest are abandoned: a trading bot, a chatbot, test images, the predecessor's dashboard and flow
- engine, and the bootstrap's first builder and controller images.
- - Together they mark between 5.9 and 7.5 GiB that nothing else marks. The two bounds depend on how the
- blobs they share among themselves are counted.
-
-**A finding beside the question.** The store's tools classify the three running images as
-*unrecorded*, the same word they use for an image nothing uses. The reason is that the catalogue pins
-them under an older name of the store, and the mesh did not make them. Nothing removes an unrecorded
-manifest today, so nothing is at risk. But any later decision to let unrecorded images go by some
-other rule would take running images with it. That is recorded as
-[issue 323](../../04-ISSUES/323-an-image-a-definition-pins-under-an-older-name-of-the-store-reads-as-unrecorded/00-report.md).
-
-## What is recommended
-
-- **Keep** the three running images. Point their modules at an image the mesh builds, where one exists,
- so the running images become the mesh's own. Two of the three apps already have a build of their own
- on a branch.
-- **Delete the two stale catalogue modules**, then treat their images like the rest.
-- **Let go of the 35 repositories and 2 manifests** once a decision says how an image the mesh did not
- make may be removed. ADR 0189 §3 forbids every tool from doing it today, and that is intended. The
- decision is the operator's: the images are the operator's, and some can no longer be rebuilt.
-- **Adopt none of them as a module.** Every app in use already has a module.
-
-## Open questions
-
-- Should an image the mesh did not make be removed through the controller's records, by a person
- recording it (as ADR 0257 lets a person record a copied base), or through a separate verb of the
- store's tools?
-- Should the older tags in a repository that is in use (about 0.8 GB together) go before their modules
- are moved to the mesh's own build, or with it?
diff --git a/01-RESEARCH/035-the-images-from-before-the-mesh/01-the-images.md b/01-RESEARCH/035-the-images-from-before-the-mesh/01-the-images.md
deleted file mode 100644
index 257a4e71..00000000
--- a/01-RESEARCH/035-the-images-from-before-the-mesh/01-the-images.md
+++ /dev/null
@@ -1,64 +0,0 @@
-# 035 — The images, one by one
-
-Read on 2026-10-08. Sizes are the bytes a repository's manifests mark. *Only theirs* is what no other
-manifest in the store marks, which is what removing the repository would free. A machine is named by its
-role.
-
-Dates: *created* is an image's build date, from a copy in a machine's local image store matched by tag;
-*commit* is the date of the commit the tag names; *not known* means neither was found.
-
-## In use
-
-| what | size | manifests | newest | in use by | named by | recommendation |
-|---|---|---|---|---|---|---|
-| an invoicing app's API | 1.41 GB | 3 tags | created 2023-01 | the anchor, its API container (newest tag, matched by digest) | its catalogue module, pinned under an older name of the store; also the frozen runtime base of the app's own build on a branch | keep the newest; let the two older go (about 0.7 GB); move the module to the mesh's own build |
-| the same app's web front | 60.5 MB | 3 tags | created 2023-01 | the anchor, its web container (matched by digest) | its catalogue module, likewise | keep the newest; let the two older go |
-| a static company website | 474 MB | 3 tags | created 2022-08 | the anchor, the website container (matched by digest) | its catalogue module, likewise; the site's repository has its own build on a branch, not yet delivered | keep the newest; let the two older go (about 75 MB) |
-
-## Pinned only by a stale module
-
-| what | size | manifests | newest | in use by | named by | recommendation |
-|---|---|---|---|---|---|---|
-| a magazine site, its older image | 393 MB | 3 tags | created 2025-10 | nothing: the anchor runs the mesh-built image | a catalogue module whose definition no machine uses | let the two older go; the newest once the stale module is deleted |
-| a photo app's older client | 11 MB | 1 | created 2024-10 | nothing: the anchor runs the mesh-built client | two per-person photo modules, assigned to no machine | delete the two modules, then let it go |
-
-## Used by nothing
-
-Each row is one repository, unless it says otherwise.
-
-| what | size (only theirs) | manifests | newest | superseded by, or why abandoned | recommendation |
-|---|---|---|---|---|---|
-| a car-listing scraper | 2.31 GB (1.43 GB) | 3 | commit 2026-02 | the module's own build, which the home server runs | let go |
-| a chatbot's API | 3.04 GB | 1 | before 2024-09 | abandoned since 2024 | let go |
-| a chatbot's web front | 19.8 MB | 1 | before 2024-09 | abandoned | let go |
-| a comic app's API and web front (2 repositories) | 828 MB | 1 each | created 2026-09 | the predecessor's; no module | let go, unless the app is wanted back (then build it as a module) |
-| a photo app's earlier API and web front (2 repositories) | 519 MB | 4 each | not known | the photo modules | let go |
-| a photo app's server and admin client (2 repositories) | 445 MB | 1 each | created 2024-10 | the photo modules' own builds | let go |
-| a map game's API and web front (2 repositories) | 121 MB | 3 each | commit 2026-02 | renamed to the travel game below | let go |
-| a travel game's API and web front (2 repositories) | 104 MB | 2 each | commit 2026-02 | no containers or data remain | let go (it can be rebuilt from its repository) |
-| a racing-circuit site | 264 MB | 3 | created 2026-06 | the module's own build, which the home server runs | let go |
-| an older company website | 400 MB | 1 | not known | the static website above, and its own build | let go |
-| a payment-link site | 52.6 MB | 3 | before 2025-11 | the module's own build | let go |
-| a mail-forwarding service | 58.5 MB | 1 | created 2026-03 | its repository's own module, assigned nowhere | let go |
-| a snake game and a text game (2 repositories) | 88 MB (0.8 MB) | 1 each | created 2026-08 and 2026-09 | the modules' own builds | let go |
-| a trading bot | 85 MB | 3 | commit 2022-08 | abandoned | let go |
-| a trading bot's scheduled jobs and web front (2 repositories) | 83 MB | 1 each | not known | abandoned (setup last changed 2025-01) | let go |
-| three base images of a former owner namespace (3 repositories) | 188 MB | 1 each | not known | abandoned (setup last changed 2023-05) | let go |
-| test images (3 repositories) and a capability test | 135 MB | 1 to 3 | not known | tests of the predecessor | let go |
-| the predecessor's dashboard and flow-engine API (2 repositories) | 165 MB (157 MB) | 2 and 3 | created 2026-09 | the predecessor is retired | let go |
-| the bootstrap's first builder and controller images (2 repositories) | 30 MB (26 MB) | 1 each | created 2026-09 | the build agent; the controller runs as a bundle | let go |
-| a build agent's and the controller's former server images (2 repositories) | 62 MB (55 MB) | 2 each | created 2026-09 | the same | let go |
-| one manifest in the mesh-built tools image's repository | 87 MB (6.6 MB) | 1 of 4 | not known | the three recorded beside it | let go |
-| one manifest in the route proxy's server repository | 9.9 MB (6.0 MB) | 1 of 6 | not known | the kept build the machines declare | let go |
-
-**Together**, the rows used by nothing mark between 5.9 GiB and 7.5 GiB that nothing outside them marks.
-The upper bound sums each repository's own bytes. That counts twice a blob two of them share. The lower
-bound subtracts every shared blob, and that also subtracts blobs shared only among these rows, which
-removing all of them would free. Removing the two stale modules' images adds 0.4 GiB. Removing the older
-tags of the three images in use adds about 0.8 GB.
-
-**Not determined.**
-
-- A push date for 14 of the repositories.
-- Whether the forge listing of 50 repositories is all of them. A second page came back empty, so I think
- it is.
diff --git a/01-RESEARCH/036-everything-over-nats/00-overview.md b/01-RESEARCH/036-everything-over-nats/00-overview.md
deleted file mode 100644
index ca46624a..00000000
--- a/01-RESEARCH/036-everything-over-nats/00-overview.md
+++ /dev/null
@@ -1,150 +0,0 @@
----
-status: active
-initiated: 2026-10-08
-touches:
- - the bus
- - 03-DESIGN/01-to-be/25-the-bus-on-nats.md
- - 03-DESIGN/01-to-be/32-what-a-module-declares.md
- - 03-DESIGN/01-to-be/45-a-core-that-cannot-fail-silently.md
- - 03-DESIGN/01-to-be/46-the-conversation-with-the-operator.md
- - 03-DESIGN/00-as-is/14-a-modules-state.md
- - 02-DECISIONS/0201-a-module-keeps-its-current-state-in-key-value-buckets-it-declares-and-reaches-through-the-runtime.md
- - the controller's records
- - the catalogue's module manifests
- - 03-DESIGN/01-to-be/54-the-controllers-records-on-the-bus.md
-became: []
----
-
-# 036 — Everything moves over NATS
-
-## What is investigated
-
-This effort asks two questions:
-
-1. Where do the mesh's modules still talk to each other around the bus?
-2. What does a **view** onto the bus need? A view is a web app or a terminal UI built from the bus
- alone. *View* is proposed as a glossary word for this.
-
-The operator set three principles on 2026-10-08:
-
-1. **All communication between modules goes over NATS.** A module may keep its own state. Another
- module reading its files, calling it over HTTP or a socket, sharing env files, running `docker exec`
- in its container, or using ssh is a gap. Only a channel's outside link, an external API, leaves the
- bus.
-2. **The approval flow, for a person's decisions, is NATS-native and generic.** Another effort is
- designing it. This one lists only how it fits ([evidence §6](01-evidence.md#6-how-the-approval-flow-fits)).
-3. **Views are built from the bus.** State lives in key-value buckets, with events that say what
- changed, and history lives in streams. A browser reaches the bus by NATS WebSocket and an auth
- callout. Every module's important state should therefore be watchable on the bus.
-
-## Why
-
-[Design 25](../../03-DESIGN/01-to-be/25-the-bus-on-nats.md) §1 already says "the bus is where the mesh
-happens", and that conditions and observed state belong in buckets anything may watch. Two parts of
-that are now built:
-
-- **Buckets for a module's state** ([ADR 0201](../../02-DECISIONS/0201-a-module-keeps-its-current-state-in-key-value-buckets-it-declares-and-reaches-through-the-runtime.md)).
-- **The controller's conditions**, kept on the bus.
-
-This effort measures how far the rest is from the principles, so that the next decisions start from
-evidence.
-
-## What was found
-
-The detail is in [01-evidence.md](01-evidence.md), whose gap table is §5. The target state is in
-[02-utopia.md](02-utopia.md). These are the gaps, ranked by how much they stand between the mesh and
-the three principles:
-
-1. **Most of the controller's state cannot be watched (G2).**
- - Plans, builds, artifacts, node facts, assignments, settings, data items, heals, durations and
- licences live in the controller's databases.
- - They can be read only by calling a verb again and again.
- - Settings change without any event, and `settings_history` holds 0 rows.
- - Only the conditions follow the bucket-plus-event pattern a view needs, and they are the template
- for the rest.
-2. **Nothing outside the private network can reach the bus as a person (G1).**
- - There is no WebSocket listener and no auth callout.
- - There is one account of password users, composed into a static file and reloaded by signal.
- - No grant is shaped for a view.
- - Until this exists, no view can be built from the bus.
-3. **The bus is one server with one copy of everything (G3).**
- - There is no cluster and no leaf node, and every stream and bucket has one replica.
- - A restart of the control-node stops every tool call, declaration and event in the mesh.
- - A laptop away from the private network has no bus at all, even between its own modules.
-4. **Modules reach each other through side doors (G5).**
- - `artifact-store-tools` runs `docker exec` in the registry's container.
- - The backup holder dumps stores by `docker exec`.
- - Two modules hold the container runtime's socket.
- - forticlient calls the resolver's verbs over a unix socket.
- - route-adapter writes into the proxy's directory.
- - Eight media modules carry copied code that calls other apps' HTTP APIs.
-5. **Some mesh state and addresses live off the bus (G4).**
- - The controller's facts snapshot is kept as a tag in the artifact store and read over HTTP.
- - The lab is given the forge's address as a hand-written setting.
- - What the registry holds is never announced.
- - The bytes of artifacts and git repositories are a reasonable named exception. Their index is not.
-6. **The bus does not watch itself (G7).** There is no system account and no dead-letter stream,
- although design 25 promises one. These live numbers are unexplained and nothing raises them:
- - 2 slow consumers;
- - 4 688 JetStream API errors;
- - 4 407 connections in 39 hours, against 16 open;
- - about 40 writes a second to a delivery bucket of 998 keys.
-
- *2026-10-08:* measured again and registered as issues
- [326](../../04-ISSUES/326-mesh-delivery-writes-a-delivery-whether-or-not-it-changed/00-report.md) (the
- delivery bucket's writes),
- [327](../../04-ISSUES/327-every-controller-verb-opens-its-own-bus-connection-so-the-connection-count-hides-a-reconnect-loop/00-report.md)
- (the connections), [328](../../04-ISSUES/328-jetstream-api-errors-are-counted-and-attributed-to-nothing/00-report.md)
- (the API errors), [329](../../04-ISSUES/329-an-event-consumer-a-module-stops-declaring-stays-on-the-bus/00-report.md)
- (the stale consumers) and [330](../../04-ISSUES/330-the-dead-letter-stream-design-25-promises-does-not-exist/00-report.md)
- (the dead-letter stream). The slow consumers are recorded under
- [issue 270](../../04-ISSUES/270-phase-1-watches-signals-that-come-later-and-hears-only-the-controllers-faults/00-report.md),
- whose open question 2 they share.
-7. **Smaller gaps:**
- - Modules' own databases are invisible to views (G6).
- - Buckets keep one value and events last a week, so history is short (G8).
- - Lists asked of verbs meet the 1 MiB message limit (G9).
- - The controller keeps two unused HTTP ports (G10).
-
-**Much already passes the test.** The forge is wrapped by its module, and pull requests arrive as
-events. No module calls the vault. No module uses ssh to reach another. Apps using a database or an
-API through a provision are acceptable by the principle.
-
-## What it touches
-
-- **The bus's design and server configuration:** accounts, WebSocket, the callout, leaf nodes,
- replicas and the system account.
-- **The controller's records**, and which of them it mirrors to buckets and events.
-- **What a module declares:** state, history streams, and tools in place of side doors.
-- **The conversation with the operator**, where the approval flow meets a view.
-
-## Decided so far, and what stays open
-
-**This effort stays active.** It graduates in parts, each part a decision that extends it without
-closing it:
-
-- **G7, the bus watching itself:** [ADR 0264](../../02-DECISIONS/0264-a-message-a-consumer-gives-up-on-is-kept-until-a-person-delivers-it-again-or-drops-it.md)
- (the dead-letter stream) and [ADR 0265](../../02-DECISIONS/0265-a-verb-that-reads-is-answered-on-the-serving-controllers-own-connection-and-every-connection-says-what-it-is.md)
- (one connection per controller, named), with issues 326 to 330.
-- **G2, the controller's state:** [ADR 0289](../../02-DECISIONS/0289-the-controller-mirrors-its-records-into-buckets-written-only-on-a-change-and-audited-against-its-store.md), *2026-10-10*. The controller
- keeps five record mirrors (nodes, assignments, walks, builds and settings), written only on a change and
- audited against its store, designed in [to-be 54](../../03-DESIGN/01-to-be/54-the-controllers-records-on-the-bus.md)
- and built under issue 419. Its rule that a bucket is written only when what it describes changed holds
- for every bucket; the conditions bucket breaks it today, which issue 420 tracks. This answers open
- question 1 below: G2 came first.
-
-Still open here: G1 (people reaching the bus, open questions 2 and 3), G3 (one bus server, open question
-4) and the views, which the operator has parked; and G4, G5, G6 and G8 to G10, which no decision has taken
-yet. The effort is graduated when each has a decision or is dropped with why.
-
-## Open questions for the next step
-
-1. **Order.** Which comes first: G2 for the controller, which makes views possible, or G1, which
- makes them reachable? This effort suggests G2. It is useful to the mesh's own modules without any
- browser.
-2. **Accounts for people.** Does a person's view get its own NATS account, with imports from the
- mesh account, or stay a user in the one account under a narrower grant? Design 25 §4 chose one
- account when only modules were users.
-3. **Which module serves the auth callout,** and which login it trusts: the mesh's identity provider,
- or the proof of to-be 46 §10.
-4. **Whether leaf nodes come before or after a cluster.** The laptop argues for leaf nodes first.
diff --git a/01-RESEARCH/036-everything-over-nats/01-evidence.md b/01-RESEARCH/036-everything-over-nats/01-evidence.md
deleted file mode 100644
index 9e682df3..00000000
--- a/01-RESEARCH/036-everything-over-nats/01-evidence.md
+++ /dev/null
@@ -1,330 +0,0 @@
-# 036 — Evidence: where the mesh still talks around the bus
-
-Measured on 2026-10-08, read-only. Three sources were used:
-
-- **The code.** The catalogue (116 module manifests and their code), the media catalogue (12), the
- controller, the node-engine, the tool runner and the SDK, all at their trunk heads of that day.
-- **The running bus.** It was read through the `nats` module's tools (`nats_server`, `nats_streams`,
- `nats_buckets`, `nats_users`, `nats_connections`, `nats_subscriptions`, `nats_backlog`).
-- **The running store.** It was read through `mesh-store.databases` and `mesh-store.query`, for table
- names and row counts only.
-
-No machine was logged into and nothing was changed.
-
-The test applied throughout is the operator's first principle. **A module may keep its own state, but
-another module reaches it only over the bus.** A module that reads another's files, calls it over HTTP
-or a socket, shares an env file with it, runs `docker exec` in its container or uses ssh to reach it is
-a gap. Only a channel's outside link, an external API, leaves the bus.
-
-## 1. What is already on the bus
-
-This section is the baseline, because most of the mesh already passes the test.
-
-| fact | measured |
-|---|---|
-| server | one NATS server, release 2.11, on the control-node; 16 connections at the time of reading |
-| streams | 21: CONTROL (work queue), NODES and ASSIGNMENTS (last per subject), EVENTS (7 days, 10 000 per subject), two seat work queues, and 15 key-value buckets |
-| EVENTS | 183 205 messages, 143 MB, 5 260 distinct subjects, 19 durable consumers |
-| key-value buckets | 15, owned by 5 modules: the controller (`calls`, `conditions`, `condition-history`, `hand-acts`, `lease`), mesh-delivery (`deliveries`, `groups`), the messenger (`open`, `sent`), claude-code (`config`, `holdings`, `servers`) and the licence manager (`bindings`), plus a `cancelled` bucket per build seat |
-| events declared | 36 of 128 modules declare `emits`; 16 declare `consumes` |
-| tool calls | every module tool and seat verb is core request/reply, `mesh.mod..tool.` and `mesh.seat..tool.`, announced in the NATS services protocol (ADR 0197) |
-| the forge's own state | the `gitea` module wraps the forge. Pull requests become events (`pull.merged`, `pull.updated`, `pull.closed`), and commit statuses are its tools. mesh-delivery reaches the forge only through those tools |
-| vault | no module calls the vault. The node-engine unseals a secret into a file, and the vault's export travels in its node's declaration |
-| ssh between modules | none. The ssh-client and claude-code modules only parse configuration (`ssh -G`) |
-
-The controller's conditions already live on the bus. They are kept in `mesh-controller_conditions`
-(802 keys) and `mesh-controller_condition-history` (90 days), and each change is announced as
-`condition-raised`, `condition-changed` or `condition-cleared`. **They are the one complete example
-of the pattern a view needs**: current state in a bucket, plus an event that says what changed.
-
-## 2. Communication that leaves the bus
-
-### 2.1 Acceptable by the principle
-
-These are an app using a provision the way it would use any database or API, or the mesh talking to
-the world outside:
-
-- **Apps on their own provisions** (about 16 modules). Postgres serves baserow, gitea, keycloak, letta,
- mailu, n8n, nextcloud and umami. The other provisions in use are mongo, S3, influx, MQTT, OIDC and
- local model access, each through `${bound::at}` and `:port`.
-- **The route data plane.** About 30 modules are forwarded by the proxy over HTTP.
-- **Desktop modules on the display server's socket.** Fifteen modules use it, and it never leaves its
- machine.
-- **Media apps sharing a library.** Nine modules share the media library directories.
-- **External APIs** reached by a channel holder (the messenger's chat service) and by the licence
- manager (the model vendor).
-
-### 2.2 Gaps
-
-The gaps are numbered for the table in §5.
-
-**Docker into another module's container, and the container runtime's socket (G5).**
-
-- **`artifact-store-tools`** runs `docker exec` inside the registry's container, which belongs to the
- `distribution` module, to read what the registry holds.
-- **The `nats` module's own tools** read `accounts.conf` with `docker exec … cat`, and fall back to
- `docker exec … wget` when the monitoring port does not answer. These calls stay inside the module,
- so they are not a gap.
-- **The backup holder (restic)** runs each module's declared `dump` command. For postgres, mongodb,
- mssql, nats and supabase, that command is a `docker exec` into the module's container. The command
- is the owning module's own declaration, so the module still chooses what is read. The channel is
- still a side door, though: the backup holder needs the container runtime's whole power over every
- container.
-- **The node-engine at genesis** reaches the controller with `docker exec mesh-controller
- ` and `docker cp`. It seeds gitea's role in the store with `docker exec psql`,
- and it reads the store's and the deprecated broker's administrator secrets from their files.
-- **Two modules mount the container runtime's socket:** `build-agent` (read-write) and `supabase`
- (read-only). The merge gate's check also mounts it into its check container. Anything holding that
- socket can act as any other module's container.
-
-**A unix socket and a shared directory between modules (G5).**
-
-- **forticlient** dials the systemd-resolved module's verb socket to route its domains. That is a
- module calling another module's verb without the bus.
-- **route-adapter** writes route files into the proxy's (traefik's) watched directory.
-
-**Mesh-written code calling another app's HTTP API (G5).**
-
-- **sonarr, radarr, lidarr and bookshelf** carry identical provisioner code that calls nzbget's,
- qbittorrent's and jackett's HTTP APIs to check a connection.
-- **The plex API** is called from the tools of sonarr, radarr, lidarr, tautulli and ombi.
-- **Status endpoints of the other apps** are called by ombi and bazarr, and home-assistant calls
- sonarr's, radarr's and lidarr's.
-
-These are eight modules in all. Each check is a question that the called module could answer as a tool
-of its own. Today the code that asks it is copied into each caller.
-
-**The artifact store over HTTP (G4).**
-
-- **The controller** speaks the OCI registry's HTTP API to hold, tag, sweep and delete artifacts.
-- **The builder** pushes images and archives, and mirrors upstream images.
-- **The node-engine** fetches bundles and archives by URL.
-- **The docker module** lists the registry, by address, as an insecure registry.
-
-This traffic is bytes by digest, which a 1 MiB message is not shaped for. Two parts of it are state
-rather than bytes, though:
-
-- **The facts snapshot** is the controller's composed record of what the mesh is, which the merge check
- judges against. It is kept as a document under an OCI tag in the registry, written and read over
- HTTP (to-be 45 §9). It is the controller's state, and it lives in another module's store.
-- **What the registry holds** is learnt by `docker exec` (above) or over its HTTP API. Nothing
- announces it on the bus.
-
-**The forge reached by address (G4).**
-
-- **The builder, the `records` module and the `lab` module** run `git clone` and `git fetch` against
- the forge. The lab is given the forge's address as a setting (`${setting:forge}`) rather than a
- provision.
-- **The node-engine at genesis** uses the forge's admin HTTP API to create its organisation, team and
- user.
-- **The builder's package registry** is the forge's npm endpoint, given as a binding.
-
-Git objects are bytes, like artifacts. The lab's setting, however, is an address written by hand,
-which is what provisions exist to replace.
-
-**The certificate authority over HTTPS.** The proxy and `ca-trust` reach step-ca's ACME directory and
-root certificates by `${bound:internal-acme-ca:…}`. ACME is the protocol the proxy speaks, so this
-belongs with §2.1. It is listed here because the trust roots are a mesh fact that could be announced
-instead.
-
-**The MCP endpoint over loopback HTTP.** claude-code points the agent at the tool runner's loopback
-port. The agent speaks MCP over HTTP, and the tool runner is the bridge onto the bus, so this is the
-designed entrance (the mesh MCP server) and not a gap.
-
-**Unused HTTP surfaces in the controller (G10).** The controller still listens on two loopback ports:
-
-- An `api` port, whose authentication is a stub that refuses every request.
-- A `board` port.
-
-Neither appears in any design as the way to read the mesh. A view built from the bus replaces both.
-
-**Mesh tool code on postgres (G6).**
-
-- **The controller** opens its three databases (`inventory`, `identity`, `licences`). The merge gate
- also creates and drops throwaway databases with an administrator credential.
-- **Four modules' tool code** opens its own database directly: `mesh-catalog`, `model-usage`,
- `claude-licence-manager` and `showcase`.
-
-Each reads only its own database, which the principle allows. The gap is not the connection. It is
-that state kept this way is invisible to anything else except through a verb (§3).
-
-`mesh-store.query` runs one read-only statement against any database the store holds. A future view
-could be tempted by it. It is an operator's diagnostic, and it would bypass every module's ownership of
-its own data.
-
-## 3. State a view could not watch
-
-A view, in the operator's third principle, reads current state from a bucket, learns that it changed
-from an event, and reads history from a stream. The table below says which of the mesh's important
-state allows that today.
-
-| state | where it lives | event on change | bucket | otherwise |
-|---|---|---|---|---|
-| conditions | bus only | raised, changed, cleared | `conditions`, `condition-history` | — |
-| hand-acts | bus only | **none** | `hand-acts` | — |
-| calls the controller answered | bus only | none | `calls` (2 000 kept, 14 days) | — |
-| the lease and epoch | bus and `controller_epoch` | none | `lease` | — |
-| deliveries and delivery groups | bus only (mesh-delivery) | `transition`, `group` | `deliveries`, `groups` | — |
-| what the operator was told | bus only (messenger) | `refused` | `open`, `sent` | — |
-| licence bindings | licence manager's database, mirrored | adopted, refused, failing, usage read | `bindings` | — |
-| **plans and walks** | `release_plan` (456 rows) | `plan-moved` | **none** | `plans`, `delivery-walks` verbs |
-| **builds and artifacts** | `build` (7 193), `build_gate`, `artifact_collected` (2 312) | `built` (from the build seat), `rolled-back` | **none** | `builds`, `artifacts` verbs |
-| **nodes: facts, reports, health** | `node`, `node_report`, `node_health`, `tunnel_peer`, `taken`; the facts snapshot in the registry | `applied` and `refused` per report | **none** | `nodes`, `node`, `status` verbs |
-| **assignments, modules, seats** | `assignment` (244), `module` (133), `seat`, `seat_holding`, `port_assignment`, `provision_pin`, `binding` | none; a membership per assignment is kept in the ASSIGNMENTS stream | **none** (ASSIGNMENTS is last-per-subject, but has no event) | `modules`, `seats`, `node` verbs |
-| **settings** | `settings` (68), `settings_history` (0 rows) | **none** | **none** | `settings` verb |
-| **data items and their readings** | `data_item` (124), `data_reading` (602) | none | none | `data` verb |
-| **heals** | `heal` (28) | `healer-acted` | none | `healers` verb |
-| **durations** | `duration` (14 617) | none | none | `durations` verb |
-| **licences, usage, audit** | licence manager's database: `licence`, `usage`, `audit`, `lease`, `offered` | adopted, failing, usage read | only `bindings` | `licences`, `usage` verbs |
-| **credentials and grants** | `bus_user` (76), `secret` (143), `module_secret` (121), `enrolment_token`, `person` | `secret-replaced` only | none | `issue`, `rotate`, `token` verbs |
-| **the bus's own health** | the server's monitoring port, on loopback | none | none | the `nats` module's tools |
-
-The most watched state is the open conditions, and it is the one example that is complete. The state
-a view of the mesh would show first is what each machine runs, how far a delivery has got, and what is
-being built. Each of these can be learnt only by calling a verb again and again. A verb's answer is
-also bounded by the server's message limit (§4.4).
-
-**Settings change with no trace.** `settings_history` has 0 rows, and no event is published when a
-setting changes. A view cannot show who changed a setting or when, and neither can the controller.
-
-**hand-acts have a bucket but no event.** A view can watch the bucket's keys, but other modules cannot
-consume a hand-act through the EVENTS stream the way they consume a condition.
-
-## 4. The bus itself
-
-### 4.1 Accounts, grants and credentials
-
-- **Accounts and users.** The bus has one account (`MESH`), and its users hold passwords. The
- controller mints each password and seals it to the recipient. It keeps a bcrypt hash and composes
- every user, with explicit publish and subscribe lists and `allow_responses` (one answer, one
- minute), into `accounts.conf`.
-- **Reload.** The node-engine writes that file into the `nats` module's directory. The module's
- entrypoint checks the file's digest every five seconds and sends the server SIGHUP when it changes.
-- **What is not configured.** The code has no nkeys, no JWT accounts, no operator and no auth callout.
- Design 25 §4 rejected the JWT model on purpose for a single tenant.
-- **20 bus accounts were live.** There are three per machine (`node.`, `.node-tools`
- and `.build-agent`), plus the controller and the control-node's own module users.
-- **The grants are wide.** The tool runner on one machine carries the union of every module assigned
- there: up to 170 publish and 599 subscribe grants. What keeps one module's code from acting as
- another's on the same machine is the tool runner, not the server.
-- **People.** A person's bus account exists in the design (design 25 §7), and the `person` table has
- 0 rows. The operator reaches the bus through the mesh MCP server, which uses the machine's tool
- runner account.
-
-**What a view in a browser would need, and is missing:**
-
-- **A WebSocket listener.** None is configured. The server configuration has `port`, `http`, `tls`,
- `jetstream` and `include` and nothing else.
-- **A way to admit a person** with a short-lived credential and a grant derived from what that person
- may see. The NATS mechanism for this is the auth callout, and it is not configured. Without it, each
- browser would need a password in the static file, and the file would be rewritten and reloaded for
- each login.
-- **A grant shape for a view:** read a set of buckets and subscribe to a set of event subjects. No such
- grant is composed today. The nearest are the module state grants of ADR 0201.
-
-### 4.2 One server, one copy
-
-- **No cluster, no leaf nodes, no replicas.** The server runs with none of these. No stream or bucket
- sets replicas, so every one has a single copy (the server reports `ha_assets: 0`).
-- **The control-node is a single point of failure for every link.** All 16 connections cross the
- private network to that one machine, at round trips of 33 to 94 ms from the other machines.
-- **What stops when it restarts.** Every tool call, declaration, report and event stops. Design 25
- §11 names leaf nodes as "deliberately out of scope; noted so it is not forgotten".
-- **Backups.** JetStream state is protected by a nightly snapshot of each stream through the snapshot
- API (ADR 0235). The live state between two snapshots has one copy, on one disk.
-- **The laptop.** It travels and sleeps behind a VPN client. While it is away from the private network
- it has no bus at all, even for tool calls between modules on itself.
-
-### 4.3 Observability
-
-- **No system account.** The bus has none, so nothing hears the server's own events about other
- users: slow consumers, permission violations, connections and disconnections. The controller hears
- only two JetStream advisories, `CONSUMER.MAX_DELIVERIES` and `CONSUMER.DELETED`.
-- **Who reads the monitoring port.** The monitoring port (8222) is published on the control-node's
- loopback. Only the `nats` module's tools read it, when asked. Nothing turns what it says into a
- condition.
-
-Numbers from the live bus that nothing currently raises:
-
-- **2 slow consumers** since the server started 1 day 14 hours earlier.
-- **4 688 JetStream API errors** out of 6.08 million calls.
-- **4 407 connections** in those 39 hours, against 16 open. The tool runners' connections were 2 to 39
- minutes old when read. *I think* a tool runner reconnects often, because its connection age was far
- below the server's uptime; the cause is not yet known. Reading the tool runner's journal for
- disconnect reasons would find out.
-- **About 5.7 million messages** delivered on the subscription to `$KV.mesh-delivery_deliveries.>` in
- those 39 hours. That is about 40 a second, for a bucket of 998 keys. *I think* mesh-delivery rewrites
- unchanged deliveries on every pass. Comparing each value's revision with its content would confirm
- it. Every view that watches the bucket would receive the same stream.
-
-Two further findings:
-
-- **No dead-letter stream.** Design 25 §3 says a message that exceeds `max-deliver` feeds
- `mesh.events.dead`, its own small stream. No such stream exists. The advisory is heard by the
- controller and kept nowhere else.
-- **Stale consumers.** Four durable consumers, one per machine's claude-code, filter on
- `claude-licence-manager` events `licence.rotated` and `licence.switched`. No manifest emits either,
- and none of the four has delivered a message. The bindings bucket replaced them (ADR 0201). ADR 0230
- leaves deleting a consumer to a person.
-
-### 4.4 The message limit
-
-- **The limit.** `max_payload` is the server default, 1 MiB. No configuration sets it.
-- **Issue 314.** Answers above the limit were dropped in the sender (issue 314). Both the controller
- and the tool runner now page a large answer. The holder keeps the pages for five minutes, and the
- caller asks again with a `Mesh-Page` header.
-- **Trimming.** Verbs that grow, such as `conditions`, `status`, the self-check and the registry
- listings, are also trimmed.
-- **Other ceilings.** A module's state value is capped at 256 KiB and a bucket at 64 MiB. A kept call
- is capped at 68 KB.
-- **What this means for a view.** A view that reads a bucket per key never meets the limit. A view
- that asks a verb for a whole list does.
-
-### 4.5 Retention
-
-- **Buckets keep one value.** Every bucket keeps history 1: the newest value per key, and no earlier
- ones.
-- **Events last a week.** EVENTS keeps 7 days and at most 10 000 messages per subject. Condition
- history keeps 90 days and hand-acts keep 90 days.
-- **No longer record.** No stream keeps what happened beyond those windows. A view of "what happened
- last month" has the store's tables (builds, plans, durations) and nothing on the bus.
-
-## 5. The gap table
-
-The effort column is a rough size: **S** is a pull request or two, **M** is a design section and a few
-pull requests across repositories, and **L** is a decision record, a design and work in several
-repositories.
-
-| # | gap | where | evidence | impact | suggested direction | effort |
-|---|---|---|---|---|---|---|
-| G1 | No way for a browser or a person's client to reach the bus | the `nats` module's server configuration; the controller's account composition | no WebSocket listener; no auth callout; one account of static password users; `person` 0 rows | no view onto the bus can be built; every view today would need a backend that reads verbs | a WebSocket listener behind the proxy; an auth callout served by a mesh module that checks the operator's identity provider and issues a short-lived user with a view-shaped grant | L |
-| G2 | Most of the controller's state is not watchable | the controller's `inventory` and `licences` databases | §3: plans, builds, artifacts, nodes, assignments, settings, data, heals, durations, licences have no bucket; settings and assignments have no event | a view must poll verbs; nothing else in the mesh can react to a plan, a node's facts or a setting changing; settings changes leave no trace | for each, a bucket of current state written on change and an event naming what changed, the store remaining the controller's record; conditions are the template | M each, L in total |
-| G3 | One bus server, one copy of everything | the `nats` module on the control-node | no cluster, no leaf nodes, replicas 1 everywhere, `ha_assets: 0` | a restart of one machine stops every link in the mesh; the laptop away from the private network has no bus at all | a leaf node per machine, so local traffic and tool calls survive the hub; later a three-node cluster with replicated streams for what must not be lost | L |
-| G4 | Mesh state and addresses kept off the bus | the facts snapshot in the registry; the forge address as the lab's setting; the registry's contents | §2.2: the snapshot is an OCI tag over HTTP; `${setting:forge}`; the registry is read by `docker exec` or HTTP | the merge check depends on the registry for the controller's state; a hand-written address does not follow the forge | the facts snapshot in a bucket or JetStream object store; the forge as a provision; the registry announcing what it holds as events. The bytes of artifacts and git stay off the bus as a named exception | S–M |
-| G5 | Side doors between modules | `artifact-store-tools`, restic's dumps, the node-engine's genesis, `build-agent` and `supabase` on the container runtime's socket, forticlient's socket, route-adapter's files, 8 media modules' HTTP checks | §2.2 | a module acts on another's container, socket or files with no grant the bus can check and nothing announced; code is copied between modules | each owning module serves the act as a tool (`distribution` lists what it holds, each store module serves its dump, the resolver's verbs over the bus, each download client answers "is this reachable with this key"); the socket kept only where building containers is the module's job | M |
-| G6 | State behind a module's own database, seen only through its verbs | `claude-licence-manager`, `model-usage`, `mesh-catalog`, `showcase` | each opens its own postgres; licences, usage and audit have no bucket | allowed by the principle, but invisible to a view | the same pattern as G2, per module: a declared bucket (ADR 0201) for the current state a person would look at | S per module |
-| G7 | The bus does not watch itself | the `nats` module; the controller's advisories | no system account; no dead-letter stream; 2 slow consumers, 4 688 API errors, 4 407 connections in 39 h, unexplained; four consumers with nothing to consume | a failing or churning link is noticed only when someone asks | a system-account user for the `nats` module that turns the server's own events into conditions; the dead-letter stream design 25 promises; the reconnect rate and the delivery bucket's write rate investigated | S–M |
-| G8 | History is short and in the store | every bucket keeps history 1; EVENTS keeps 7 days | §4.5 | a view of what happened beyond a week reads tables, not the bus | a history stream per kind of change that a view needs (deliveries, plans, builds, hand-acts), with its own retention, declared like a bucket | M |
-| G9 | Answers are bounded by one message | the server's default `max_payload` | 1 MiB; paging since issue 314 | a view that asks verbs for lists pages; one that reads buckets does not | let views read buckets, not verbs; keep paging for verbs | S |
-| G10 | Unused HTTP surfaces | the controller's `api` and `board` ports | `api` refuses every request; neither is named in a design | dead code that suggests a second way in | remove both once a view exists | S |
-
-## 6. How the approval flow fits
-
-Another effort is designing the approval flow: a person's decision, asked and answered on the bus,
-generic over what is being approved. It continues to-be 46 §6 (asks) and §10 (asks that authorise)
-under ADR 0234 and ADR 0258. This effort does not duplicate it. It notes only where it meets the
-three principles:
-
-- **An open ask is current state.** It belongs in a bucket, keyed by the ask, so a view lists what
- waits for the operator and watches it change. The messenger's `open` bucket is the precedent.
-- **An answer is an event.** The proof it carries is checked by the controller, never by the view
- that collected it (ADR 0258: a click proves only that something at the session chose).
-- **A view that lets the operator answer** needs G1's person-shaped credential. That credential lets
- the controller tell the operator's own answer from anything else that holds the session.
-
-## 7. What this effort did not read
-
-- **The TypeScript of the tool runner** (`node-tools/src`). The Go part was read.
-- **Each media app's own code beyond its provisioner and tools.** The apps themselves are upstream
- software and are judged by §2.1.
-- **Live traffic per subject**, beyond the subscription counters the server keeps.
diff --git a/01-RESEARCH/036-everything-over-nats/02-utopia.md b/01-RESEARCH/036-everything-over-nats/02-utopia.md
deleted file mode 100644
index 6bebff90..00000000
--- a/01-RESEARCH/036-everything-over-nats/02-utopia.md
+++ /dev/null
@@ -1,81 +0,0 @@
-# 036 — The target: a mesh that happens on the bus
-
-This is where the gaps in [01-evidence.md](01-evidence.md) point. It is a picture to test proposals
-against, not a design. Each paragraph names the gap it answers.
-
-## Every link between modules is a subject
-
-A module owns its state: its files, its database, its container. It is reached only by its tools, its
-seat's verbs, its events and its buckets.
-
-- **No module acts on another's container, socket or files.** A store module serves its own dump. The
- registry says what it holds. The resolver's verbs are called over the bus like any other seat's. A
- download client answers whether it is reachable with a key (G5).
-- **Every act is grantable.** Because every act is a subject, the bus can refuse it, and the controller
- can say who may perform it.
-
-The bus is not for bulk bytes, and two kinds of traffic stay off it by name:
-
-- **Bulk bytes stay where they are.** The bytes of an artifact and of a git repository, and an app
- using its database, keep their own protocols.
-- **Who reaches them is decided on the bus.** Where to reach them is a provision, and what exists is
- announced on the bus (G4).
-
-## Every important state is watchable
-
-For each thing a person would look at, the owner keeps three things on the bus:
-
-1. **A bucket** of current state, one key per thing: a node, an assignment, a plan, a build, a
- delivery, a setting, a licence, an open ask.
-2. **An event** for each change, naming the thing and what changed. Another module can consume it, and
- a view can redraw from it.
-3. **A history stream**, where the past matters, with a retention the owner declares (G8).
-
-The store remains the owner's record where it needs one. The bucket is written from it on change, never
-instead of it. The open conditions already work this way. Plans, builds, nodes, assignments, settings
-and licences come next (G2, G6). A verb is still how something is asked or done. It stops being the
-only way to see.
-
-## Views are built from the bus
-
-A view is a web page or a terminal UI that shows part of the mesh. *View* is proposed here as the
-glossary word for this. It has no backend of its own:
-
-- **It connects** to the bus over WebSocket, through the proxy, as the person using it.
-- **It reads** the buckets it shows, watches them, and subscribes to the events that explain a change.
-- **It acts** by calling the same tools and verbs an agent calls. A decision travels as an answer to
- an ask, carrying its proof, and the controller checks it (the approval flow, §6 of the evidence).
-
-The person is admitted by an **auth callout** served by a mesh module. That module checks the person's
-login with the mesh's identity provider and issues a short-lived bus user. The user's grant is derived
-from the view's declaration: these buckets to read, these events to hear, these tools to call. No
-password is written into the server's file for a person, and a person's access ends when the credential
-expires (G1).
-
-## The bus survives a machine
-
-Every machine runs a **leaf node**:
-
-- **Local traffic stays local.** A module's connection is local, and tool calls between modules on one
- machine keep working when the hub is away, as they must on a laptop behind a VPN client.
-- **What must not be lost is replicated.** The control-node's server is one of three in a cluster, and
- the declarations, the control stream and the controller's buckets are kept on three disks rather than
- one. The nightly snapshot remains the guard against our own mistakes, not against a disk (G3).
-
-## The bus watches itself
-
-- **Server events become conditions.** The `nats` module holds a system-account user and turns the
- server's own events into conditions, like any other probe: a slow consumer, a permission refused, a
- client reconnecting in a loop, a consumer that nothing feeds.
-- **Exhausted messages are kept.** A message delivered as often as allowed lands in a dead-letter stream
- that a view can show (G7).
-- **Nothing to rescue by paging.** Answers stay within one message because views read keys, not
- whole lists (G9).
-
-## What is not wanted
-
-- **No view with a database of its own** that copies the mesh's state. That copy drifts, which is the
- thing the mesh exists to prevent.
-- **No HTTP API on the controller beside the bus** (G10).
-- **No second signing hierarchy** for its own sake. The callout issues users under the account the mesh
- already has, unless a separate account for people turns out to be the simpler grant.
diff --git a/01-RESEARCH/037-the-catalogues-hard-coded-preferences/00-overview.md b/01-RESEARCH/037-the-catalogues-hard-coded-preferences/00-overview.md
deleted file mode 100644
index d9608253..00000000
--- a/01-RESEARCH/037-the-catalogues-hard-coded-preferences/00-overview.md
+++ /dev/null
@@ -1,49 +0,0 @@
----
-status: graduated
-initiated: 2026-10-08
-touches:
- - 02-DECISIONS/0262-a-preference-has-a-default-in-its-manifest-and-the-operators-own-value-has-none.md
- - 03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md
- - the catalogue's module manifests
- - the media catalogue's module manifests
-became:
- - 02-DECISIONS/0262-a-preference-has-a-default-in-its-manifest-and-the-operators-own-value-has-none.md
----
-
-# 037 — The catalogue's hard-coded preferences
-
-## What is investigated
-
-[ADR 0262](../../02-DECISIONS/0262-a-preference-has-a-default-in-its-manifest-and-the-operators-own-value-has-none.md)
-lets a manifest give a **preference** a default, which the mesh-wide settings and a node's settings
-override. Before it, a value a person might want to tune per machine could only be written into a
-module's file as a literal. This effort lists those literals in both catalogues: the value, where it
-is, the key and default it would take as a preference, and why a person would change it.
-
-## Why
-
-The first case was the notifier's font size, which is less than half as tall on a laptop panel of
-about 240 DPI as on a desktop monitor of about 110 DPI, while X is told 96 on both. The same is true of
-every size, font and gap on the desktop, and of retention counts, timeouts and thresholds in the
-services. A list says which modules to convert, in what order, and which values are shared between
-modules and should keep one default.
-
-## What it touches
-
-The manifests of both catalogues, and through ADR 0262 the controller's `settings` verb, which lists
-every declared preference. Nothing is changed here: each conversion is a change to its module, made on
-the operator's yes.
-
-## Documents
-
-- [01 — the survey](01-the-survey.md): every candidate, by module, strongest first, with the modules
- that already take settings and those that have nothing to convert.
-
-## What it became
-
-*Closed 2026-10-10.* The survey was the deliverable, under [ADR 0262](../../02-DECISIONS/0262-a-preference-has-a-default-in-its-manifest-and-the-operators-own-value-has-none.md), and each conversion is a change to its module made on
-the operator's yes. Three things the survey found are not conversions: the rows marked *(shell)* wait on
-the controller filling `${setting:…}` in login-shell code, tracked by issue 410; a value several modules
-share (the interface font size, the accent colour) needs a decision on one shared preference, tracked
-by issue 415; and the backup program's retention and the container runtime's log rotation need their
-programs to read a file, or a new default, first, tracked by issue 416.
diff --git a/01-RESEARCH/037-the-catalogues-hard-coded-preferences/01-the-survey.md b/01-RESEARCH/037-the-catalogues-hard-coded-preferences/01-the-survey.md
deleted file mode 100644
index d94ade01..00000000
--- a/01-RESEARCH/037-the-catalogues-hard-coded-preferences/01-the-survey.md
+++ /dev/null
@@ -1,115 +0,0 @@
-# 01 — The survey
-
-Both catalogues were read on 2026-10-08: every manifest's file contents and the sources they are
-copied from, the shell code modules contribute to the login session, and the contributions to the
-display session. Nothing was changed. Each row is a literal that is a preference in the sense of
-[ADR 0262](../../02-DECISIONS/0262-a-preference-has-a-default-in-its-manifest-and-the-operators-own-value-has-none.md):
-true of the software, and only tuned per machine or per mesh. The proposed default is always today's
-value, so converting a module changes no machine until a value is set.
-
-Values that are the operator's own (names, addresses, accounts, secrets) are left out, as are values the
-mesh assigns (`${port:…}`, `${dir:…}`, `${machine:…}`, `${bound:…}`, `${secret:…}`) and structure nobody
-tunes. Every proposed key passes ADR 0262's whole-word check.
-
-**Where a literal sits decides what converting it needs.** A row marked *(shell)* is in code a module
-contributes to the login shell, and one marked *(contribution)* is in a contribution to the display
-session. ADR 0262 fills `${setting:…}` in files, contributions and served facts, but not in shell code,
-so the *(shell)* rows need the controller to fill shell code too before they can convert. A row marked
-*(compiled)* is in a module's own program and needs the program to read the value from a file first.
-
-## The desktop, strongest first
-
-| Module | Where | Today | Key | Why a person changes it |
-|---|---|---|---|---|
-| xorg | the X resources file | `Xft.dpi: 96` | `dpi` | A laptop panel of about 240 DPI and a desktop monitor of about 110 DPI want different values; the file's own comment already expects a setting |
-| xterm | X resources *(shell)* | `faceSize: 12`, face JetBrainsMono Nerd Font, `saveLines: 10000` | `font-size`, `font`, `scrollback-lines` | Terminal text size per screen; the face and history are taste |
-| i3 | its configuration | `font pango:JetBrainsMono Nerd Font 11` | `font-size` | Title text size follows the screen's density |
-| i3 | its configuration | `gaps inner 0`, `gaps outer 0`, `default_border pixel 1`, resize step 10 px | `inner-gap`, `outer-gap`, `border-width`, `resize-step` | Taste and density; a 1-pixel border is hard to see on a dense panel |
-| i3 | its configuration | focused colour `#de5200` | `accent-colour` | Taste; the same colour is in the bar and the launcher (see below) |
-| i3status-rust | the display-session contribution *(contribution)* | bar font size 11 | `font-size` | The bar's height follows it |
-| i3status-rust | the top bar | window title cut at 50 characters | `title-width` | How much title fits depends on the screen's width |
-| i3status-rust | the bottom bar | disk warning at 850 GB, critical at 920 GB | `disk-warning`, `disk-critical` | Thresholds written for one disk; wrong on a smaller laptop drive |
-| i3status-rust | the bottom bar | updates warn at 10, polled every 1800 s; clock format with seconds, every 1 s; container block every 2 s, disk every 20 s | `updates-threshold`, `updates-interval`, `clock-format`, `refresh-interval` | Polling costs battery; the clock is taste |
-| rofi | its theme | font size 11, width 600 px, 8 lines, icon 24 px, padding 10 px, message font size 11 | `font-size`, `width`, `lines`, `icon-size`, `padding`, `message-font-size` | The launcher's size per screen |
-| rofi | the power menu theme | width 800 px, font size 32 | `power-menu-width`, `power-menu-font-size` | Too large on a small panel. The power menu script also passes 350 px, which overrides 800: one of the two is dead |
-| rofi | its configuration | history of 25 | `history-size` | History length |
-| screen-lock | the session's X settings *(shell)* | lock after 1800 s; display standby 1800 s, off 3600 s | `lock-after`, `display-standby-after`, `display-off-after` | The clearest laptop and desktop difference |
-| screen-lock | its wrapper script | blur 5, ring width 7.0, accent `#ca4a00`, time and date formats | `blur`, `ring-width`, `accent-colour`, `clock-format`, `date-format` | Look, density and locale |
-| picom | its configuration | terminal opacity 0.95 focused and 0.75 not; tooltip 0.95; fade step 0.05; corner radius 0 | `terminal-opacity`, `inactive-terminal-opacity`, `tooltip-opacity`, `fade-step`, `corner-radius` | Taste |
-| picom | its configuration | `backend = "glx"` with vsync | `backend` | Depends on each machine's graphics driver |
-| adwaita | GTK 3 and 4 settings, Qt settings, and the GNOME settings *(shell)* | interface font Inter 11, monospace 11; cursor size 24 (also in X resources and GNOME settings); double-click 400 ms, 3 scroll lines | `font-size`, `cursor-size`, `double-click-interval`, `scroll-lines` | Interface size on a dense panel; the cursor size must change in all four places together |
-| fonts | the fontconfig defaults | monospace JetBrainsMono Nerd Font, sans Inter, serif Noto Serif | `monospace-face`, `sans-face`, `serif-face` | Taste; every desktop module names these faces again |
-| the laptop's hardware module | touchpad configuration and its script | acceleration 0.15, tapping on, natural scrolling on | `touchpad-speed`, `tap-to-click`, `natural-scrolling` | Pointer feel; the script repeats 0.15 |
-| the laptop's hardware module | the power daemon's battery levels | low 15 %, critical 10 %, act at 7 %, action suspend | `battery-low`, `battery-critical`, `battery-action-at`, `battery-action` | Thresholds move as a battery wears |
-| the laptop's hardware module | its scripts | brightness step 5; on-screen display for 1500 ms | `brightness-step`, `notification-timeout` | Feel |
-| feh | the wallpaper command | `--bg-fill` | `wallpaper-mode` | Fill, scale or centre is taste |
-| zsh | its rc file | `HISTSIZE` and `SAVEHIST` 100000 | `history-size` | History length |
-| lemurs | its configuration | background black, border white | `background-colour` | Low value; only the start of the file was read |
-
-## Services
-
-| Module | Where | Today | Key | Why a person changes it |
-|---|---|---|---|---|
-| restic | its program *(compiled)* | keep 14 daily, 8 weekly, 6 monthly | `keep-daily`, `keep-weekly`, `keep-monthly` | Retention is the clearest service preference, and today it cannot be changed without a new build |
-| logrotate | its configuration | weekly, keep 4; the login record monthly, keep 1 | `rotate-period`, `rotate-count`, `wtmp-rotate-count` | Retention per machine and disk |
-| docker | the prune unit and timer | prune what is older than 168 h, weekly | `prune-age`, `prune-schedule` | How long images and build cache stay |
-| docker | the daemon's configuration | no log rotation at all | `log-size`, `log-files` | **A new value, not a conversion:** container logs grow without bound today. A default such as 10 MB and 3 files changes every machine, so it is a decision of its own |
-| memory-pressure | the zram generator | `min(ram / 2, 16384)` | `zram-size` | RAM and workload |
-| memory-pressure | the out-of-memory daemon and the session's limit | swap used 90 %, pressure 60 % for 20 s; the session 80 % | `swap-used-limit`, `pressure-limit`, `pressure-duration`, `session-pressure-limit` | How early processes are killed |
-| time-sync | its servers | a regional pool | `pool-region` | The region is the mesh's |
-| pacman | its configuration and the mirror list's options | 5 parallel downloads; a country list, the latest 20, sorted by rate | `parallel-downloads`, `mirror-countries`, `mirror-count` | Bandwidth and region |
-| fail2ban | its jails | ban 1 day after 3 failures in 1 day; repeat offenders 4 weeks after 2 in 2 weeks | `ban-time`, `find-time`, `max-retries`, `repeat-ban-time`, `repeat-find-time`, `repeat-max-retries` | Strictness |
-| sshd | its configuration | port 22 | `listen-port` | A port people move. The filter and the jail name the port too, and must follow it |
-| dnsmasq | its configuration | two public resolvers | `upstream-resolvers` | The choice of resolver |
-| icecast | its configuration | client timeout 30 s, source 10 s, burst 65535, queue 524288, log level 3, log size 10000 | `client-timeout`, `source-timeout`, `burst-size`, `log-size` | Streaming tuning |
-| mailu | its environment file | message size 50 MB, 200 messages a day, sign-in rate limits, fetch every 600 s, log level | `message-size-limit`, `message-rate-limit`, `sign-in-rate-limit`, `fetch-delay`, `log-level` | Real limits a person sets |
-| matrix | the homeserver's configuration | request size 20 MB | `upload-size-limit` | The upload cap |
-| supabase | three files | a sign-in lifetime of 3600 s, written three times | `session-lifetime` | One value in three places, which a setting keeps equal |
-| supabase | the functions' main | 150 MB, 60 s | `function-memory-limit`, `function-time-limit` | Function limits |
-| power | its stop unit and script | 120 s, 30 s | (weak) | Bounds on shutdown work, rarely changed |
-
-## The media catalogue
-
-| Module | Where | Today | Key | Why |
-|---|---|---|---|---|
-| qbittorrent | its web login script | ban for 60 s | `ban-duration` | How long a failed login is locked out |
-| tautulli | its start script | wait 60 s for the media server | `media-server-wait` | Low value |
-
-The other media modules carry only identities from settings, key-provisioning scripts and mergeable
-files. Nothing to convert.
-
-## Already taking settings
-
-- **`${setting:…}`:** the notifier (font size and width, the first preferences), power, hostname, xdg,
- keycloak, mailu, lab, and every media module.
-- **A mergeable file:** baserow, claude-code, the licence manager, confluence, gitea, gitlab, grafana,
- home-assistant, icecast, influxdb, jira, keycloak, letta, mailu, matrix, mesh-watcher, messenger,
- nextcloud, nodered, records, route-adapter, searxng, step-ca, supabase, unifi; in the media
- catalogue bazarr, bookshelf, jackett, kometa, lidarr, nzbget, ombi, qbittorrent, radarr, sonarr and
- tautulli. A mergeable file's content already is its defaults; ADR 0164's declaration would list its
- keys.
-
-## Nothing to convert
-
-avahi, blueman, bluetooth, build-agent, ca-trust, clipmenu (its 1-second timeout is a safety bound),
-cups, dbus, de-spiegel, disk-load, distribution, dmenu, gnome-keyring, hello-web, invoicing, letta,
-local-model-consumer, localization (the language and keymap are the operator's, not a preference),
-mesh-catalog, minio, model-usage, mongodb, mosquitto, mssql, n8n, nats, networkmanager, nftables,
-node-env, only-office, redis, route-proxy, ssh-client, sudo, systemd, systemd-networkd,
-systemd-resolved, triggerhappy, umami, xclip. A container's internal port is structure, not a
-preference.
-
-## What follows from it
-
-- **Shared values want one default each.** The accent colour is `#de5200` in the window manager, the
- bar and the launcher, and `#ca4a00` in the lock screen. The interface font size is 11 in the window
- manager, the bar, the launcher and the GTK, Qt and GNOME settings, and 12 in the terminal. As one
- setting per module they would drift. Either each module declares the same default and a person sets
- them together, or the mesh grows a preference several modules read. That is a decision, not a
- conversion.
-- **Three prerequisites in the controller and the modules:** filling `${setting:…}` in shell code, for
- the *(shell)* rows; the restic program reading its retention from a file; and, for the container
- runtime's log rotation, a decision on the new default.
-- **Order:** the density rows first (xorg's DPI, then the font sizes of the window manager, the bar,
- the launcher, the terminal and the interface, and the cursor size), because the laptop panel is the
- case that started this. Then the lock and display timeouts, then retention and thresholds.
diff --git a/01-RESEARCH/038-shares-and-mounts-the-mesh-manages/00-overview.md b/01-RESEARCH/038-shares-and-mounts-the-mesh-manages/00-overview.md
deleted file mode 100644
index 0298b153..00000000
--- a/01-RESEARCH/038-shares-and-mounts-the-mesh-manages/00-overview.md
+++ /dev/null
@@ -1,82 +0,0 @@
----
-status: graduated
-initiated: 2026-10-08
-touches:
- - 04-ISSUES/169-a-machine-shares-its-files-and-the-mesh-does-not-know/00-report.md
- - 04-ISSUES/154-a-machines-own-network-is-not-a-reach/00-report.md
- - 01-RESEARCH/027-the-system-layer-as-modules/02-candidates-and-questions.md
- - 03-DESIGN/01-to-be/26-the-seats.md
- - 03-DESIGN/01-to-be/48-a-module-says-how-it-is-healthy.md
- - 02-DECISIONS/0045-a-machine-firewall-is-the-sum-of-what-it-listens-on.md
- - 02-DECISIONS/0051-shared-data-is-the-operators.md
- - 02-DECISIONS/0092-an-operator-delivers-a-pair-credential.md
- - the catalogue's module manifests
- - the controller's seat set
-became:
- - 03-DESIGN/01-to-be/52-shares-and-mounts.md
- - 02-DECISIONS/0263-a-share-is-a-provision-a-node-mounts-it-on-demand-and-only-nodes-that-ask-get-it.md
----
-
-# 038 — Shares and mounts the mesh manages
-
-## What is investigated
-
-How the mesh shares a directory from one node and mounts it on the others, so that no machine
-carries a hand-written export or a hand-written mount line. Three kinds of source are in scope:
-
-1. **A node's share.** The home server shares its storage pool over NFS. A node that wants the files
- mounts them.
-2. **A device outside the mesh that is not always there.** A gaming device on the home network shares
- a folder over SMB, and only while it is switched on. A USB disk is the same case.
-3. **What the mesh should show and say** when a mount works, when it does not, and when its source is
- simply away.
-
-## Why
-
-On 2026-10-08 the operator found two failed mounts on the workstation, and said two things:
-
-- about the media library: *"This should be handled by the mesh. If a node exposes an NFS share, it
- should get configured on all the nodes in their fstab?"*
-- about the gaming device's share: *"This mount only works if I plug in my recalbox machine."*
-
-They also set a requirement: **the home server shares its storage pool over NFS**, the workstation's
-library mount comes from that share, the export reaches only the mesh's private network, it squashes
-root, and the media modules' data (the Plex library is sacred) must not be put at risk.
-
-[Issue 169](../../04-ISSUES/169-a-machine-shares-its-files-and-the-mesh-does-not-know/00-report.md)
-predicted the first failure on 2026-09-30: nothing declares the share's ports, so the packet filter
-closes them. This effort measures what happened, and proposes the decision issue 169 asked for.
-
-## What was found
-
-The detail is in [01-evidence.md](01-evidence.md). In short:
-
-1. **The library mount times out because the home server's packet filter drops NFS.** The server
- exports the library and its NFS service runs. The mesh's filter accepts only what a module
- `listens` on, and no module listens on 2049, 111 or 445. The server's NFS counters read zero I/O
- since its last boot six days earlier, so no client has used either share since then.
-2. **The gaming device's mount fails because the gaming device is off.** It is an SMB share on the device itself,
- not a disk. The fstab line has no `nofail` and no automount, so each boot records a failed unit.
- The line also carries the share's password in a world-readable file.
-3. **The export is not where a person would look.** The server's `/etc/exports` holds only comments.
- The export comes from a property of the ZFS dataset, which writes its own file in `/etc/exports.d`.
- The library is a child dataset of the pool, so an export of the pool's root does not show the
- library's files unless it crosses into child filesystems.
-4. **User ids do not match across nodes.** The library is owned by a service account and group. The
- operator's account has the same id (1000) on every node, but on the home server that id belongs to
- a different account, which owns the pool's backup repository. NFS with `sec=sys` trusts the client's
- id. A plain export of the whole pool would give the workstation's operator account the backup
- repository.
-5. **No module, and until now no tool, covers any of this.** No module in either catalogue exports or
- mounts anything. No mesh tool read a machine's fstab, its mount units or its exports, so this effort
- read them through the login shell. A read-only `mounts` module now proposes those tools.
-
-## Where it goes
-
-[02-options.md](02-options.md) weighs the options. The decision is
-[ADR 0263](../../02-DECISIONS/0263-a-share-is-a-provision-a-node-mounts-it-on-demand-and-only-nodes-that-ask-get-it.md),
-and the design is [52 — Shares and mounts](../../03-DESIGN/01-to-be/52-shares-and-mounts.md).
-
-On 2026-10-08 the operator chose **one share per folder**, so the backups are never exported, and
-**mesh machines only**, over the private network. Which nodes mount a share, and whether the laptop
-mounts read-only, is decided at assignment.
diff --git a/01-RESEARCH/038-shares-and-mounts-the-mesh-manages/01-evidence.md b/01-RESEARCH/038-shares-and-mounts-the-mesh-manages/01-evidence.md
deleted file mode 100644
index fd7945b5..00000000
--- a/01-RESEARCH/038-shares-and-mounts-the-mesh-manages/01-evidence.md
+++ /dev/null
@@ -1,134 +0,0 @@
-# 038 — Evidence
-
-Measured on 2026-10-08 on the four nodes, read only, through the mesh's tools: `node-service-manager`
-(`failed`, `status`, `journal`), `node-uplink.links`, `node-packet-filter.rules`,
-`disk-load.disk_filesystems`, `docker.docker_list`, and `node-login-shell.execute` for the files no
-tool read (§7). Addresses are given in documentation ranges: the home network (the underlay) as
-`192.0.2.0/24`. Node roles: the **home server**, the **workstation**, the **laptop**, the **control-node**.
-
-## 1. The workstation's two failed mounts
-
-`node-service-manager.failed` on the workstation lists two failed system units, both generated from
-`/etc/fstab` (their unit file is under `/run/systemd/generator`, and `mesh_declared` is false):
-
-| unit | the fstab line (password removed) | what the journal says |
-|---|---|---|
-| the library | `: nfs defaults,_netdev 0 0` | `Mounting timed out. Terminating.` 90 s after boot, at every boot since 2026-10-04; the last successful mount was at the boot of 2026-08-23 |
-| the gaming device | `///Share cifs username=root,password=…,iocharset=utf8,sec=ntlmssp 0 0` | `mount error(113): could not connect to ` at every boot |
-
-Neither line has `nofail` or `x-systemd.automount`. Both name the source by its underlay address.
-`remote-fs.target` is inactive on the workstation, because both of its members failed.
-
-## 2. Why the library times out
-
-- The home server exports the library, and its NFS and Samba services run. `nfs-server` and `smb`
- are active and enabled, and ports 2049, 111 and 445 are listening on every address.
-- **The mesh's packet filter drops them.** The mesh's table on the home server has `policy drop` on
- its input chain for the underlay link and the private network link. It accepts a port only where a
- module `listens` on it ([ADR 0045](../../02-DECISIONS/0045-a-machine-firewall-is-the-sum-of-what-it-listens-on.md)).
- No rule names 2049, 111 or 445.
-- From the workstation, a TCP connection to the home server's port 2049 fails, and one to port 22 on
- the same address succeeds.
-- The home server's NFS counters (`/proc/net/rpc/nfsd`) read `io 0 0` since its boot on 2026-10-04.
- No established connection to 2049 or 445 exists. **Nobody has used either share since that boot.**
-
-This is the failure [issue 169](../../04-ISSUES/169-a-machine-shares-its-files-and-the-mesh-does-not-know/00-report.md)
-predicted: the shares close silently once the filter is the sum of what modules listen on.
-
-## 3. What the home server exports, and from where
-
-- `/etc/exports` holds only the distribution's comments; it was last written on 2026-09-20.
-- The export comes from ZFS. The library dataset has `sharenfs=rw=@192.0.2.0/24` and `sharesmb=on`.
- ZFS writes `/etc/exports.d/zfs.exports` itself, marked *do not edit*. The kernel's export table
- (`/var/lib/nfs/etab`) holds one entry: the library, to the underlay range, `rw,sync,root_squash,
- no_all_squash,secure,no_subtree_check,sec=sys`.
-- The Samba configuration has one share, `media`, on the library, `valid users = media`,
- `force user = media`, writable.
-- **The pool is two filesystems.** The pool is mounted at its root, and the library is a child dataset
- mounted under it. An NFS export of the pool's root does not show the library's files unless the
- export crosses into child filesystems (`crossmnt`); without it, a client sees an empty directory
- where the library should be.
-
-## 4. What lives on the pool, and who uses it
-
-The pool's root holds five directories:
-
-| directory | owner | what it is |
-|---|---|---|
-| the library (child dataset, about 90 TiB used) | the media account and group, `0775` | movies, series, anime, music, audiobooks and more |
-| downloads | the media account and group | the download clients' and the library managers' working area |
-| archive | root | the operator's archive |
-| a second Plex folder | the operator's account on the home server | an older library folder |
-| the backup repository | the operator's account on the home server, `0700` | the restic repository the backup seat writes |
-
-Plex's own configuration and database live on a separate disk, not on the pool.
-
-Eight running containers bind paths on the pool, all of them as plain bind mounts of local paths:
-Plex (the library's folders, all read-only), the library managers for series, movies, music and
-subtitles, two download clients, and one automation app. **Exporting the pool over NFS changes none of
-these paths and none of their owners.** An export adds a way in; it does not move or chown anything.
-The only way an export endangers this data is by what it lets a client do (§5).
-
-## 5. User and group ids across the nodes
-
-| node | id 1000 | the media account | the media group |
-|---|---|---|---|
-| home server | a separate account, which owns the backup repository and the older Plex folder | exists, owns the library | exists |
-| workstation | the operator | exists, same ids as on the server | exists; the operator is a member |
-| laptop | the operator | absent | absent |
-| control-node | the operator | absent | absent |
-
-The library managers on the home server run as the media account and group, set by their `puid` and
-`pgid` settings.
-
-With `sec=sys`, the server believes the id a client sends; `root_squash` changes only root. So:
-
-- **A client's operator account (1000) is the home server's other account (1000).** An export of the
- pool's root would let it read and write the backup repository and the older Plex folder.
-- **A file written by the laptop's operator lands owned by 1000**, not by the media account, and the
- library managers may then fail to rename or delete it.
-- A device on the underlay that is not a node can claim any id. That is a reason to export to the
- private network only, where every address is a node.
-
-## 6. The gaming device's share
-
-- The gaming device is a separate machine on the underlay, a retro-gaming device that shares a folder named
- `Share` over SMB. It is not a USB disk. The router's DNS knows its name. It was switched off during
- the survey and did not answer a ping.
-- The workstation has `cifs-utils`, `avahi` and `nss-mdns`, and its `hosts:` line contains
- `mdns4_minimal`. **The packet filter drops inbound mDNS** on every node, as the `avahi` module's
- README already records. So discovery by mDNS cannot see the gaming device arrive today. Waiting for it
- would first need a local-link reach for `listens`, which is the question of
- [issue 154](../../04-ISSUES/154-a-machines-own-network-is-not-a-reach/00-report.md).
-- The share's password is written in `/etc/fstab`, which is mode `0644`. Every account on the
- workstation can read it.
-
-## 7. What the catalogue and the tools cover
-
-- **No module exports or mounts.** No manifest among the 117 modules of the core catalogue and the 12
- of the media catalogue writes an export, a Samba share, a mount unit or an fstab line.
-- **`disk-load.disk_filesystems`** lists NFS and CIFS filesystems that are mounted now, with their space.
- It cannot see a mount that failed, an fstab line, a mount unit or an export.
-- **`node-service-manager`** shows a mount unit's state and journal, but not what it mounts or with
- which options.
-- **No tool read a machine's fstab, its mount and automount units with their sources, or its NFS and
- SMB exports.** This effort read them with `node-login-shell.execute`, which runs a shell command as
- the operator account. That is a work-around for a missing tool, so a read-only `mounts` module with
- two tools, `mounts_list` and `mounts_exports`, was opened in the catalogue alongside this effort. It
- redacts passwords in mount options and never touches a network mount point, so a hung server cannot
- hang the tool.
-
-## 8. Prior work
-
-- **Issue 169** (open, the operator's proposal of 2026-09-30): two seats, `nfs-share` and `smb-share`,
- because the two protocols share an intent and not a contract; a consumer module that writes a
- systemd `.mount` unit from its binding; the modules that use the files never learn about NFS; the
- data-loss case of a writer starting before its mount; and identity crossing the wire.
-- **Research 027**, option M2: each client module writes its own systemd mount and automount unit, and
- `/etc/fstab` stays the machine's.
-- **Research 026**, the tools list: `exports`, `shares`, `clients`, `reload` on the server;
- `mounts`, `mount`, `unmount`, `test` on the client.
-- **ADR 0092**: a credential for something outside the mesh is an operator-delivered secret, accepted
- into the vault (`secret accept`) and never replaced by a made one.
-- **`${machine:mesh-range}`** already exists as a placeholder in two manifests, so an export line can
- name the private network's range without naming a node.
diff --git a/01-RESEARCH/038-shares-and-mounts-the-mesh-manages/02-options.md b/01-RESEARCH/038-shares-and-mounts-the-mesh-manages/02-options.md
deleted file mode 100644
index df6fb9c5..00000000
--- a/01-RESEARCH/038-shares-and-mounts-the-mesh-manages/02-options.md
+++ /dev/null
@@ -1,146 +0,0 @@
-# 038 — Options
-
-Each question below lists the options weighed and the one recommended. The recommendations together
-became [ADR 0263](../../02-DECISIONS/0263-a-share-is-a-provision-a-node-mounts-it-on-demand-and-only-nodes-that-ask-get-it.md).
-
-## Q1. Who writes the export
-
-| option | for | against |
-|---|---|---|
-| A. Keep the ZFS `sharenfs` property, and have a module declare only the ports | nothing new on disk | the export lives in a dataset property, beside the operator's data; a module would be setting properties on data the mesh does not own (ADR 0051); the options are one string with no room for squashing |
-| B. A module owns `/etc/exports` whole | one file, one owner | the file is the machine's; a second exporter on the same node would collide |
-| **C. A module, `nfs-server`, writes its own file in `/etc/exports.d`, and the ZFS property is turned off when the module's export is live** | the mesh owns exactly its file; the distribution reads the directory; unassigning removes the file | a migration step: the property must be cleared by an act, not by hand |
-
-**Recommended: C.** The module holds a node seat, `nfs-share`, so one module exports on a node, and it
-provides the provision `nfs-share` (issue 169's proposal). Samba is left as found, with its own seat
-`smb-share` later; nothing on the mesh uses it, and the gaming device's SMB share is a client case (Q6).
-
-## Q2. What is exported, and to whom
-
-The operator asked for the pool's root. Exporting it plainly has two problems (evidence §3 and §5): the
-library is a child filesystem, so the export needs `crossmnt` to show it; and with `sec=sys`, any client
-account with id 1000 becomes the server's own 1000, which owns the backup repository.
-
-| option | for | against |
-|---|---|---|
-| A. The pool's root, `rw,root_squash`, to the underlay range | what the operator literally asked; what exists today (but for the library only) | gives the backup repository to every client's operator account; open to every device on the underlay, which can claim any id |
-| B. The pool's root, `rw,crossmnt,all_squash`, every client mapped to the media account, to the private network only | one share, as asked; every file written lands owned by the media account; the `0700` backup repository is unreadable to the media account, so it stays closed | every client writes as one account, so there is no per-person permission on the share; top-level folders other than the library are writable by every client |
-| **C. One share per subtree: the library and the downloads, each `rw,all_squash` to the media account, to the private network only; the backup repository and the archive not exported** | the backup repository is not reachable at all; ownership is right; a consumer mounts only what it needs | two shares instead of one; a later folder needs a new share entry (one setting) |
-| D. Kerberos (`sec=krb5`) with id mapping | real per-person identity | needs a KDC and keytabs on every node; far beyond the problem |
-
-**Recommended: C, with B as the operator's alternative if one share is wanted.** Both use
-`all_squash` with `anonuid` and `anongid` set to the owner of the exported path, read on the server
-(`${access::uid}`, from issue 153's fix), never written as numbers in a manifest. Both export to
-`${machine:mesh-range}` only, so every client is a node, reached over the private network. `root_squash`
-is implied by `all_squash`. `sync`, `secure`, `no_subtree_check` stay as they are today. The server
-serves NFS version 4 only, so only port 2049 is opened, `from: mesh`; 111 and the mountd ports stay
-closed.
-
-**What this does not touch:** the local bind mounts of Plex and the library managers (evidence §4),
-the datasets' owners and modes, and the Samba share.
-
-## Q3. How a node mounts a share
-
-| option | for | against |
-|---|---|---|
-| A. The mesh writes lines into `/etc/fstab` | the file people know | one shared file and one ordered list; the node-engine can place a marked block but every line competes with the operator's |
-| B. `autofs` with its own maps | mature automounter | a second daemon doing what systemd already does |
-| **C. A `.mount` unit and an `.automount` unit per share, written by one module, `mounts`, which holds the node seat `node-mounts`** | a unit is a unit: its state, its journal, its health; an automount never holds the boot; unassigning removes it; no shared file | units have to be named by the escaped mount path, which the module computes |
-
-**Recommended: C.** The `.automount` is enabled under `remote-fs.target`; the `.mount` is not enabled
-on its own, so nothing mounts at boot and nothing fails at boot. The mount is made at first access and
-released after an idle time. The options carried:
-
-- `x-systemd.automount` semantics through the `.automount` unit, `TimeoutIdleSec` (default 10 minutes);
-- a mount timeout, `TimeoutSec` on the `.mount` (default 30 s for a node's share, 10 s for an
- occasional one), instead of systemd's 90 s;
-- `_netdev` and an order after the private network's link is up;
-- for NFS, `vers=4.2,hard,timeo=150,retrans=3` on a read-write share (no silent write loss) and
- `soft` allowed for a read-only one, as a setting.
-
-An automount also closes issue 169's data-loss case: the mount point is always an autofs mount, so a
-process that writes there either triggers the mount or gets an error. It never writes into the empty
-directory underneath.
-
-## Q4. Which nodes get a share
-
-The operator suggested every node. Weighed:
-
-| option | for | against |
-|---|---|---|
-| A. Every node mounts every share | no assignment to make | the control-node is the stably reachable machine the controller runs on, and gains a writable path into the library for no use; the laptop, behind a corporate VPN client, would try a mount whenever a file manager looks at the path |
-| B. A node mounts a share only when a module on it requires it | precise | a person who wants the files in their file manager has no module that requires them |
-| **C. A node mounts what its `mounts` assignment lists; the list may be set once at the mesh-wide layer** | opt-in per node, and "every node" is one setting at the mesh-wide layer, with a node's own layer to leave a share out | two layers to read when asking why a node has a share; `mounts_list` says which layer gave it |
-
-**Recommended: C, assigned to the workstation and the laptop, not to the control-node.** A module that needs
-the files (a player, an indexer) requires `nfs-share` as well; the controller then adds the share to
-that node's `mounts` unit set, so B is the special case of C, not a second way.
-
-## Q5. Health
-
-| case | finding | condition |
-|---|---|---|
-| a node's share, automount armed, server answers | healthy | none |
-| a node's share, server's 2049 unreachable from this node for 5 minutes | not healthy | `share-unreachable`, a warning: *"The home server's share `library` is not reachable from the workstation since 19:21."* |
-| a node's share, the mount failed with an error other than a timeout | not healthy | the same condition, with the error |
-| an occasional source, absent | **absent**, a state, not a finding | none |
-| an occasional source, present but the mount fails | not healthy | `share-unreachable` |
-| the server: the export table lacks a declared export, or `nfs-server` is down | not healthy | the module's ordinary health condition |
-
-The `mounts` module's code probes each source's port on a timer and keeps each share's state in its
-own key-value bucket (ADR 0201): `mounted`, `armed`, `absent`, `unreachable`, with since when. Its
-health reads that bucket. Nobody reads its files.
-
-## Q6. Occasional sources
-
-The gaming device is not a node, so it cannot hold a module or provide a provision. Weighed:
-
-| option | for | against |
-|---|---|---|
-| A. A fstab line with `nofail,x-systemd.automount` | small | a hand-written line again, and the password stays in the file |
-| **B. An external source in the `mounts` assignment's settings: protocol, name, share, mount point, presence `occasional`, and the name of a credential** | the same unit pair and tools as a node's share; the password goes to the vault | a share with no provider, so no binding: the module checks the name itself |
-| C. Wait for the device by mDNS and mount when it appears | mounts before anyone asks | the packet filter drops mDNS today (evidence §6); depends on issue 154 |
-
-**Recommended: B now, C when issue 154 gives `listens` a local-link reach.** With an automount, nothing
-has to happen when the gaming device appears: the next access mounts it. The probe only keeps the state
-honest, so `mounts_list` says *"the share `retro-games` is not present since 18:02"*. A USB disk is the same kind of source,
-with presence taken from its device unit (`BindsTo=` on the disk's by-uuid device) instead of a probe.
-
-**The credential:** the share's password is an operator-delivered secret, accepted into the vault with
-`secret accept` (ADR 0092), and reaches the node as a file the node-engine unseals (ADR 0086). The unit
-names it as `credentials=`. Nothing in a repository or in `/etc/fstab` holds it.
-
-## Q7. Taking over the existing fstab lines
-
-The two lines are the operator's. The node-engine's own `.mount` unit in `/etc/systemd/system` already
-takes precedence over the generated unit of the same name, but the line would still be read by every
-other tool and person.
-
-**Recommended:** the `mounts` module gets an act, `adopt`, that for one mount point:
-
-1. checks a unit pair the module placed is armed for that mount point, and refuses if not;
-2. comments out the fstab line, with a marker naming the module and the date, and keeps a copy of the
- file beside it;
-3. reloads the service manager and says what it did.
-
-It runs only on a person's word. The library line is adopted after the home server's share works over
-the private network; the gaming device's after its credential is accepted.
-
-## Q8. Over the bus
-
-Every exchange between the modules is over NATS (research 036):
-
-- the controller wires provider and consumer through the provision; the binding carries `at` (the
- server's private-network name), `path` and `options`;
-- `nfs-server` keeps its exports and the clients it sees in its bucket, and answers `test` (does it
- export this path to this node) by request and reply;
-- `mounts` keeps each share's state in its bucket; a view or the controller watches it;
-- no module reads another's files (ADR 0260).
-
-## The operator's answers (2026-10-08)
-
-1. **The pool's root, or one share per folder?** One share per folder (Q2 option C), so the backup
- repository is never exported.
-2. **Underlay clients?** Mesh machines only, over the private network.
-3. **Every node, or the nodes that ask? Read-write from the laptop?** Decided at assignment, which is the
- operator's; the recommendation (Q4 option C; the laptop read-only) stands in the design.
diff --git a/01-RESEARCH/039-a-tool-that-needs-a-persons-approval/00-overview.md b/01-RESEARCH/039-a-tool-that-needs-a-persons-approval/00-overview.md
deleted file mode 100644
index ed45259e..00000000
--- a/01-RESEARCH/039-a-tool-that-needs-a-persons-approval/00-overview.md
+++ /dev/null
@@ -1,111 +0,0 @@
----
-status: graduated
-initiated: 2026-10-08
-touches: []
-became:
- - 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
- - 03-DESIGN/01-to-be/46-the-conversation-with-the-operator.md
----
-
-# 039 — A tool or verb that needs a person's approval
-
-A note to design from later, not a design.
-
-## What is investigated
-
-Whether a module's tool or a seat's verb can declare, in its manifest, that **calling it needs a
-person's approval**, and the mesh then refuses the call without one.
-
-The operator's idea, 2026-10-08: *"I think we need to be able to mark tools as 'requires human
-approval'."*
-
-The shape, as first seen:
-
-- **The declaration is the tool's own.** The manifest marks a tool or verb as needing approval, beside
- the rest of what it declares about itself.
-- **The mesh enforces it, not the MCP client.** The tool runner, or the controller for a seat verb,
- refuses the call unless it carries a **warrant**: the router's record that a person chose one option
- of one ask, from the flow ADR 0259 sets out (hq pull request 211, not yet merged). An agent may ask;
- it cannot act alone. A client-side prompt is no boundary, because any tool runner may call any tool
- on any node (ADR 0175).
-- **The ask carries** what is to be done, why, the exact arguments, and who asks. Its options are
- Approve and Decline, Approve at the `approve` **assurance level**. On a warrant for Approve the call
- goes ahead **once, with those arguments, before the warrant's expiry**; anything else is refused.
-- **Where it is answered** until a channel that declares a verified sender (Telegram) is live: at the
- operator's desk, or at the controller's terminal. Open: the desk answers only `acknowledge` options
- ([ADR 0258](../../02-DECISIONS/0258-a-desk-click-performs-only-an-acknowledgement-and-an-answer-is-a-one-time-ticket.md)),
- so the desk alone cannot approve.
-
-## Why now
-
-On 2026-10-08 the only thing between an agent and acts like a snapshot of the media library, a
-whole-mesh push or the bus's upgrade was the agent asking in chat before it called the tool. Nothing in
-the mesh would have refused the call. The same day, several holes were found in exactly this layer:
-an agent on the control-node could become root without a person, and through the login shell's tool an
-agent on any node could run a command there as an account that can (ADR 0259 §8 as revised, and
-ADR 0266, hq pull request 225, not yet merged). Once agents run without root, **the tools are the
-boundary**, and a tool that can do lasting harm needs a person behind each call.
-
-## The first case: the login shell's `execute`
-
-The operator decided on 2026-10-08 that the login shell seat's verb `execute` is the first tool to
-need approval, on every node, once the mark exists. It runs any command it is given as the node's
-operator account, and that account has passwordless sudo, so one call is root on that node. Until the
-mark exists, the verb is removed on the control-node.
-
-The calls it carried (about 300 across the four nodes, counted on 2026-10-08): about a third ran
-something with sudo; most of the rest were reads, such as reading or searching a file, listing
-containers, asking a unit's state, listing sockets, or looking up a user or a name.
-
-So **narrow read tools must replace the common reads first**, each a tool of the module that owns the
-area. Otherwise nearly every approval asked is for a read, and the operator learns to approve without
-reading, which makes the approval worth nothing.
-
-## Candidates seen on 2026-10-08
-
-| Tool or verb | Why a person |
-|---|---|
-| the login shell's `execute` | decided: see above |
-| the new zfs module's `zfs_snapshot` | a snapshot of the media library holds its space; on a scratch dataset it does not matter |
-| `mesh-controller.push` | sends builds to nodes outside a walk |
-| the bus's upgrade, `mesh-controller.bus upgrade` | already a step a person starts ([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) rule 6); today "a person" is whoever calls it |
-| `mesh-delivery.release` | the glossary defines it as a person's word, and nothing checks that a person said it |
-| the controller's `plans` go and stop | starts or halts a walk |
-| a secret's `rotate` | can cut off every consumer of a credential |
-| the places and accesses settings | being made terminal-only; approval is the other way |
-| the media library's share and mounts (`nfs-server`, `mounts`) assignment | the library is the operator's sacred data |
-
-## What it bears on
-
-- **ADR 0259** (pull request 211): asks, the router, warrants and assurance levels. This effort adds a
- new kind of asker: the tool runner or the controller asking on behalf of a call. Who the asker is
- matters, because a warrant is issued to one asker alone.
-- **ADR 0266** (pull request 225): agents without root. It makes the tool layer the boundary this
- effort guards.
-- **The `record` upgrade policy** (ADR 0236): "a person's push" is today only a convention.
-- **The held delivery's release**: the same.
-- **Issue 336** (pull request 228, a hold nobody is told about): an approval waiting with nobody told is
- the same fault, so a waiting approval must be said, as a condition or on a channel.
-- **The hand-act log** (ADR 0234 §8): a call made on a warrant is recorded with the warrant.
-
-## Open questions
-
-1. **Per tool or per argument.** A snapshot of the library needs a person; one of a scratch dataset
- does not. Is the mark on the tool, on an argument's value, or decided by the tool from its arguments?
-2. **Who may approve.** Only the operator role exists today. Is that enough for every tool?
-3. **Replay and expiry.** One warrant, one call: how is "these exact arguments" bound to the warrant,
- and how long does a warrant stay good?
-4. **A call waiting for approval.** How is it shown: a condition, the asks list, the call's own answer?
-5. **The MCP client while it waits.** Does the call block until answered, or return "asked, waiting"
- with a handle the agent calls again?
-6. **The answer before a verified channel exists.** The desk is `acknowledge` only; is the controller's
- terminal the place, and how does it prove a person there?
-7. **Per call, or per session or time window.** For a tool called as often as `execute`, does each
- call need its own approval, or may one approval cover one agent session, or a stated span of time?
- A wider approval is less noise and more reach for whoever holds it.
-8. **Audit.** Is a warranted call a hand-act, and where is a declined or expired one recorded?
-
-## How it will be checked
-
-Measured so far: only the login shell's calls, above. The next evidence to gather: how many tools and verbs in the catalogue act on
-data or on other nodes, and of those, how many are guarded by anything but the caller's restraint.
diff --git a/01-RESEARCH/040-the-agents-home-is-the-modules-and-the-operators-rules-and-lessons/00-overview.md b/01-RESEARCH/040-the-agents-home-is-the-modules-and-the-operators-rules-and-lessons/00-overview.md
deleted file mode 100644
index f0930b12..00000000
--- a/01-RESEARCH/040-the-agents-home-is-the-modules-and-the-operators-rules-and-lessons/00-overview.md
+++ /dev/null
@@ -1,74 +0,0 @@
----
-status: graduated
-initiated: 2026-10-09
-touches:
- - 03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md
- - 03-DESIGN/01-to-be/40-building-the-operators-agent-and-its-licence-manager.md
- - 03-DESIGN/00-as-is/15-the-agent-and-its-licences.md
- - 02-DECISIONS/0216-the-agents-configuration-is-registered-through-its-module-at-three-scopes-and-served-as-one-plugin.md
- - 02-DECISIONS/0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md
- - 02-DECISIONS/0243-the-agent-module-removes-a-home-item-it-did-not-place-only-on-the-persons-word-and-keeps-a-copy.md
- - 02-DECISIONS/0245-a-verb-says-what-it-replaces-and-the-agent-is-guarded-from-working-round-the-mesh.md
- - 02-DECISIONS/0259-an-ask-is-a-message-on-the-bus-any-channel-answers-it-and-the-asker-acts-on-a-warrant.md
- - 04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/00-report.md
-became:
- - 02-DECISIONS/0273-the-agents-home-is-the-modules-and-the-operators-personal-rules-and-lessons-are-registered-through-it.md
- - 03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md
----
-
-# 040 — The agent's home is the module's, and the operator's rules and lessons are registered through it
-
-## What
-
-Three things about the coding agent's own directory under a person's home (`~/.claude`), investigated
-together because the operator raised them as one on 2026-10-09:
-
-1. **Who owns the configuration files in that directory.** [ADR 0216](../../02-DECISIONS/0216-the-agents-configuration-is-registered-through-its-module-at-three-scopes-and-served-as-one-plugin.md)
- gave the agent module a home scope that "owns only what it placed"; everything else there is held as
- found ([ADR 0182](../../02-DECISIONS/0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md)).
- The operator's direction reverses that for the configuration: *"the home-scope does own ~/.claude, so
- it should be completely empty and manual edits should be forbidden."*
-2. **Whose the rules in that directory are.** The rule files a person writes there today are the
- person's own, on one machine. The operator's direction: *"Rules in my home directory are my 'personal
- rules' which should also end up under control of the claude-code module and all other changes to
- ~/.claude should go through the claude-code module's mcp tools."*
-3. **How an agent learns from a correction.** The operator's direction: *"register the decision, but make
- sure it's clear and that you can/should be pro-active about it. For example, if I'm frustrated, try to
- see why and try to prevent it in the future by warning yourself about stuff not to do to frustrate me
- — kind of like a self-learning process."*
-
-## Why
-
-The home is the one place the agent's configuration still drifts. [01](01-what-the-home-holds-and-the-options.md)
-measured it on 2026-10-09 across the four nodes: a skill to-be 36 retired in its first version is still on
-all four; a person's instruction file is on three; the operator's two hand-written rules are on one; and
-nothing in the mesh places, watches or raises anything about any of it. The corrections the operator has
-given the agent live in the agent's own memory for one repository on one machine, 45 notes of which at least
-9 are a correction of a way of working, shown to nobody and reaching no other node.
-
-[Issue 340](../../04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/00-report.md)
-closed the tools' route into the operator's home for the right reason: a tool cannot tell the operator from
-an agent, so an agent could plant something every session obeys. Whatever owns the home must keep that
-protection. The two routes the mesh now has for the operator's own word, the controller's terminal
-([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))
-and a warrant on an ask ([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)),
-make it possible to write into the operator's home without trusting a tool's caller.
-
-The record was searched first: `records_search` for "personal rules lessons frustration self-learning" and
-for "hand edit home .claude drift condition" found nothing on 2026-10-09.
-
-## What it touches
-
-- **The agent module's design** ([to-be 36](../../03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md)
- §1 and §8): what under the home is the module's, and the scopes.
-- **ADR 0216 §5 and §6** (the home scope owns only what it placed; what it did not place is reported) and
- **ADR 0243** (removal on the person's word): the mechanism changes, the decisions stand.
-- **ADR 0182**: the class table holds; what changes is which class the configuration files are in.
-- **ADR 0245**: the guard gains a refusal, and the session gains a reminder.
-- **ADR 0259**: the module becomes an asker, for the operator's yes.
-- **Issue 340**: the protection stands; the terminal and the warrant are the two ways in.
-
-## Documents
-
-- [01 — What the home holds, and the options](01-what-the-home-holds-and-the-options.md): the measurement
- on four nodes, the agent's own memory as it stands, and the options weighed.
diff --git a/01-RESEARCH/040-the-agents-home-is-the-modules-and-the-operators-rules-and-lessons/01-what-the-home-holds-and-the-options.md b/01-RESEARCH/040-the-agents-home-is-the-modules-and-the-operators-rules-and-lessons/01-what-the-home-holds-and-the-options.md
deleted file mode 100644
index bd74b609..00000000
--- a/01-RESEARCH/040-the-agents-home-is-the-modules-and-the-operators-rules-and-lessons/01-what-the-home-holds-and-the-options.md
+++ /dev/null
@@ -1,130 +0,0 @@
-# 040 — What the home holds, and the options
-
-## 1. The agent's directory on the laptop, 2026-10-09
-
-Read by name on the laptop's own disk, contents not read. The directory holds 33 entries.
-
-**Configuration, written by a person or by the program on a person's choice:**
-
-| path | what | how it got there |
-|---|---|---|
-| `rules/` | 2 rule files (1779 and 755 bytes): how text meant to be pasted elsewhere is handed over; how git is used in a second forge's repositories | written by hand |
-| `skills/` | 5 skills of the operator's own, for the operator's other work; and `skills/synced/`, 2 folders the vendor's own sync placed | by hand; by the program |
-| `commands/` | empty | — |
-| `settings.json` | 6 keys: the attribution, auto mode, feedback drafts, the model, the spinner tips, the model switch on a flag | by hand, and by the program on a `/model` or `/config` choice |
-| `settings.local.json` | 1 key, `permissions`: the rules the program writes when a person answers "always allow" in a session | by the program, on a person's answer |
-| `plugins/` | the program's record of its marketplaces (the vendor's and the mesh's `nox-mesh`) and installed plugins, and its cache | by the program |
-| no `CLAUDE.md`, no `agents/`, no `hooks/` | the instruction file was removed on 2026-10-07 through the module's removal tool, with a copy kept (ADR 0243) | — |
-
-**Working state, written by the program for itself** (24 entries): the credentials file (the module's own
-code writes it, ADR 0183), `history.jsonl` (5 MB), `projects/` (30 directories, 1.5 GB, holding every
-project's sessions and the agent's own memory), `sessions/`, `session-env/`, `shell-snapshots/`,
-`file-history/`, `debug/`, `cache/`, `paste-cache/`, `telemetry/`, `stats-cache.json`, the daemon's lock,
-log and status files, `state/`, `ide/`, `jobs/`, `feedback/`, `backups/`, `worktrees/`, and a few markers.
-
-**The managed directory under `/etc`**, by contrast, holds exactly what the module renders: the instruction
-file, the managed settings, the managed tool-server file, and the marketplace with the `nox-mesh` plugin.
-
-## 2. The homes on four nodes, 2026-10-09
-
-Read through each node's `claude_code_config_status` (the module's own status tool), the field listing the
-home's own items beside what the mesh placed:
-
-| node | home items the mesh did not place | placed by the mesh at home |
-|---|---|---|
-| the laptop | 2 rule files, 5 skills | none |
-| the workstation | an instruction file, a `cleanup` skill, a subagent (a module reviewer) | none |
-| the home server | an instruction file, a `cleanup` skill | none |
-| the control-node | an instruction file, a `cleanup` skill | none |
-
-Three things follow:
-
-- **The retired skill is still everywhere.** To-be 36 §1 said on 2026-10-02 that the `cleanup` skill
- becomes "nothing; it named the predecessor's forge". It is on all four nodes on 2026-10-09. Nothing
- owns it, so nothing removed it.
-- **The operator's rules exist on one node.** A rule about how the operator wants text handed over is as
- true on the workstation as on the laptop. It is on the laptop alone, because the only way to put it
- anywhere is to write the file there.
-- **Nothing is raised about any of it.** The status tool lists these items when asked. No condition says
- the laptop's rules are missing elsewhere, or that a skill the design retired is still loaded in every
- session on four machines. The three mesh-wide instruction sections and the `nox-mesh` plugin (holding
- the guard alone) are the only things the mesh renders for a session, and both are in the managed
- directory, not the home.
-
-## 3. The agent's own memory, 2026-10-09
-
-The agent keeps notes for itself in the program's memory directory under `projects/` for one repository
-on the laptop: 45 notes on 2026-10-09. At least 9 of them are a correction of a way of working that the
-operator gave in a session, for example: finish a module before pushing it; use the mesh's tools, not
-ssh; read the merge gate's verdict before merging; what is done is what the operator sees working;
-no work-arounds when a tool is missing; a language choice the operator insists on. Each was learned once,
-after the operator said it, sometimes with irritation.
-
-Those notes:
-
-- are on one machine, for one repository: a session on the workstation, or in another repository on the
- laptop, starts without them;
-- are written by the agent for itself, in its own words, and shown to nobody: the operator cannot see
- what the agent taught itself, or correct it;
-- are not read before a tool runs: a note saying "read the merge gate first" is read at session start,
- if the agent opens it, and nothing says it again at the moment a merge is about to be called.
-
-The mesh keeps one related record: the guard's record of every refusal and override
-([ADR 0245](../../02-DECISIONS/0245-a-verb-says-what-it-replaces-and-the-agent-is-guarded-from-working-round-the-mesh.md) §5),
-which is how a habit that found a way round is seen. It records what the guard refused, not what the
-operator corrected.
-
-## 4. What the vendor loads from the home
-
-As documented by the vendor and confirmed on the laptop on 2026-10-09 (a session's own context names the
-two rule files as "the user's private global instructions for all projects"): the home's `CLAUDE.md` and
-every file under `rules/` are read into every session the account starts, in every project; `skills/`,
-`commands/` and `agents/` are offered under their own names; `settings.json` is the account's settings,
-below the managed settings; hooks come from `settings.json` and from a plugin. None of it is read for
-another account, and none of it reaches another machine.
-
-## 5. The options
-
-### Who owns the configuration under the home
-
-1. **As today: the module owns what it placed, the rest is found** (ADR 0216 §5, ADR 0182). Rejected by
- the operator's direction, and by §2: a rule on one node reaches no other, a retired skill stays on
- four, and nothing is raised. There is a second reason: the guard refuses an agent that reaches a
- machine by ssh, and refuses nothing that writes `~/.claude/rules/x.md`. On a node where the agent
- runs as the operator, that is a rule planted in every later session, by hand, with no record.
-2. **The module owns the whole directory and regenerates it.** Rejected in ADR 0182 (option 1), and still:
- the working state is the program's, and a render that touched `projects/` would erase a season of
- memory.
-3. **The module owns every configuration file by name, excludes the program's working state by name,
- renders the configuration from registrations, and reports a hand edit as a condition.** Chosen.
-
-### Whose the rules are
-
-1. **Per node** (the home scope as ADR 0216 defines it: one account's directory on one machine). Rejected
- as the only scope: the operator is one person on four nodes, and §2 shows what one-node rules give.
-2. **Mesh-wide instruction sections**, which already reach every node. Rejected as the place for them:
- those are the mesh's word to every session, written at the controller's terminal, about the mesh.
- A personal rule is the operator's word about the operator: how to hand over text, which tools their
- other employer's repositories need. It belongs to the person and follows the person's agent, into the
- home, under the person's own names.
-3. **A new kind, the personal rule, registered for the operator and rendered into the agent's home on
- every node.** Chosen. The same scope serves the operator's own skills, commands, subagents and settings.
-
-### How a lesson becomes a rule
-
-1. **The agent writes the rule itself.** Rejected: an agent that writes its own rules can loosen them, the
- operator never sees what it taught itself, and issue 340's reasoning holds — text that every session
- reads is written on the operator's word, never on an agent's.
-2. **Lessons stay in the agent's memory.** Rejected by §3.
-3. **A lessons register in the module. An agent proposes a lesson; the operator says yes or no; a yes
- makes it a personal rule, rendered everywhere; the agent is reminded of the rules that bear on what it
- is about to do.** Chosen.
-
-### How the operator's yes reaches the operator's home
-
-1. **Trust the tool's caller.** Rejected in issue 340.
-2. **The controller's terminal only.** Safe; kept as one of two ways. It makes a yes in a session a trip
- to the control-node, which is what the operator asked to avoid.
-3. **A warrant on an authorising ask** (ADR 0259): the module asks the operator, with the exact text
- bound in the ask, and acts on the warrant alone. Chosen beside 2. The approve option needs a channel
- that proves its sender; until one answers, the terminal is the way.
diff --git a/01-RESEARCH/041-the-agents-session-runs-confined/00-overview.md b/01-RESEARCH/041-the-agents-session-runs-confined/00-overview.md
deleted file mode 100644
index 0a469d93..00000000
--- a/01-RESEARCH/041-the-agents-session-runs-confined/00-overview.md
+++ /dev/null
@@ -1,68 +0,0 @@
----
-status: active
-initiated: 2026-10-10
-touches:
- - 02-DECISIONS/0266-on-the-control-node-agents-run-under-an-account-of-their-own-that-cannot-become-root-without-a-person.md
- - 02-DECISIONS/0259-an-ask-is-a-message-on-the-bus-any-channel-answers-it-and-the-asker-acts-on-a-warrant.md
- - 02-DECISIONS/0245-a-verb-says-what-it-replaces-and-the-agent-is-guarded-from-working-round-the-mesh.md
- - 02-DECISIONS/0268-the-login-shells-execute-is-withheld-on-the-control-node-until-a-call-to-it-needs-a-persons-approval.md
- - 02-DECISIONS/0144-anything-on-a-machine-may-call-anything-on-it.md
- - 03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md
- - 04-ISSUES/344-known-gaps-in-the-agents-boundary-accepted-for-now/00-report.md
- - 01-RESEARCH/039-a-tool-that-needs-a-persons-approval/00-overview.md
-became: []
----
-
-# 041 — The agent's session runs confined, instead of being proven by a disk-wide search
-
-## What
-
-Whether the coding agent's session on a node with an **agent account** should be **confined when it
-starts**, so that what it cannot do is enforced by the kernel, instead of the node-engine proving the
-account harmless by searching every local filesystem for setuid programs. Three parts, from the
-operator's idea of 2026-10-10:
-
-1. **No new privileges.** The session and everything it starts run with the kernel's `no_new_privs` flag,
- so a setuid or setgid program, packaged or not, and a program with file capabilities, grant it nothing.
-2. **A confined view of the files.** The session sees `/usr` read-only, its own home, a private `/tmp` and
- the few system files a program needs to run, and nothing else.
-3. **Everything else through the mesh's tools.** What an agent reads outside that view (a file, a
- directory, a log) is a tool of a seat, not a path.
-
-And then: what must be **proven** for a node to be root-free under such a start (every way into the
-account goes through it), how the node-engine checks that cheaply, and what becomes of the setuid search.
-
-## Why
-
-[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)
-§3 item 10 judges the agent account by a search of every local filesystem mounted without `nosuid` for
-setuid or setgid root programs no package owns, bounded at 15 minutes. Until a search finishes, the verdict
-is *not judged*, and [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)
-§8 rule 2 counts a machine without a recent verdict as not root-free, so the router approves nothing proven
-there and a channel's `verified-sender` is stripped.
-
-Measured on 2026-10-10 (the controller's `root-free` verb and `node` verb): on the control-node the search
-"did not finish within 15m0s", the agent account is *not judged*, and two urgent conditions are open
-(`agent-root` and `root-not-free`). Linking the operator's phone channel is blocked by it.
-[Issue 344](../../04-ISSUES/344-known-gaps-in-the-agents-boundary-accepted-for-now/00-report.md) gap 9 had
-foreseen it ("on a large data pool the root search may never finish"). A resumable search is the short-term
-fix, tracked separately. This effort asks whether the search is the right instrument at all: it answers
-"is there a program that could make this account root?", while the question ADR 0259 needs is "can an
-agent's session become root?". A confined start answers the second question directly.
-
-## What it touches
-
-- **ADR 0266 §3 and §4**: which ways to root the node-engine judges, and what a root-free verdict rests on.
- Under `no_new_privs` item 10 (unpackaged setuid) and the *not judged* file capabilities no longer
- apply to the session; new things must be proven instead (§3 of the findings).
-- **ADR 0266 §7 and to-be 36 §9**: `claude-agent` becomes the confined start, not only an account switch.
-- **ADR 0259 §8**: `root-free` would read the new proof.
-- **ADR 0245**: the guard stays as the session's own refusal; the confinement backs it in the kernel.
-- **ADR 0144, ADR 0152 and ADR 0266 §7**: the mesh MCP server has no login of its own, so a confined agent
- still reaches every tool the operator reaches. Not closed here; recorded as the boundary of this effort.
-- **Issue 344** gaps 1, 3, 4 and 9, and **research 039** (a tool that needs a person's approval).
-- **A node filesystem seat** that does not exist today.
-
-## Documents
-
-- [01 — The options, what must be proven, and the open questions](01-options-and-open-questions.md)
diff --git a/01-RESEARCH/041-the-agents-session-runs-confined/01-options-and-open-questions.md b/01-RESEARCH/041-the-agents-session-runs-confined/01-options-and-open-questions.md
deleted file mode 100644
index bdedb39b..00000000
--- a/01-RESEARCH/041-the-agents-session-runs-confined/01-options-and-open-questions.md
+++ /dev/null
@@ -1,184 +0,0 @@
-# 041 — The options, what must be proven, and the open questions
-
-Sources: [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)
-§3, §4, §6 and §7; [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)
-§8; [to-be 36](../../03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md) §9;
-[issue 344](../../04-ISSUES/344-known-gaps-in-the-agents-boundary-accepted-for-now/00-report.md);
-[ADR 0144](../../02-DECISIONS/0144-anything-on-a-machine-may-call-anything-on-it.md) and
-[ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md) (the machine's login is
-the authority on loopback). The records module was searched on 2026-10-10 for `setuid`, `not judged yet`,
-`loopback` and `cannot tell the operator from an agent`; the hits are the records above.
-`no_new_privs`, `NoNewPrivileges`, `Landlock`, `bubblewrap` and `ProtectSystem` appear in no record.
-
-What the kernel and the tools do is stated from their documentation and has **not been measured on any node**
-yet. Each such claim is marked *to measure* where the decision would rest on it.
-
-## 1. What the setuid search answers, and what it does not
-
-ADR 0266 §3 judges eleven ways to root by reading the machine. Item 10, the setuid search, is the only one
-whose cost grows with the data on the machine. On the control-node it did not finish within its 15-minute
-bound (measured 2026-10-10, `mesh-controller.root-free`), so the verdict is *not judged* and ADR 0259 §8
-blocks every approval proven there.
-
-The search asks whether a program exists that could make the account root. It does not ask whether the
-agent's session can run one. Even when it finishes, ADR 0266 lists what it leaves unseen: a packaged setuid
-program with a flaw of its own, file capabilities on a program, setuid programs on filesystems not searched.
-
-## 2. The three parts, compared
-
-### 2.1 No new privileges
-
-`no_new_privs` is a per-process flag (Linux 3.5 and later). It is set by `prctl(PR_SET_NO_NEW_PRIVS)`, by
-`setpriv --no-new-privs`, or by the service manager's `NoNewPrivileges=yes`. Any process may set it, without
-privilege. Once set it is inherited by every child and cannot be cleared. Under it, `execve` grants no
-setuid, no setgid and no file capability.
-
-| Way to root (ADR 0266 §3) | Under `no_new_privs` |
-|---|---|
-| 3, 4: a sudo or doas rule | inert: `sudo` and `doas` are setuid programs |
-| 5, 6: polkit through `pkexec` | inert (setuid); polkit **through D-Bus** to a root daemon is not touched |
-| 7, 8: a container runtime's socket or TCP port | not touched: the request goes to a root daemon |
-| 10: unpackaged setuid program | inert, on every filesystem, searched or not |
-| *not judged*: file capabilities, a flawed packaged setuid program | inert |
-| *not judged*: a root-run file the account can write; root's keys | not touched |
-
-So it removes the whole class "a program that gains privilege when executed", and leaves the class
-"**a root daemon that does something when asked**": the container runtime, the service manager over D-Bus
-(polkit), the ssh daemon (a key into another account), cron and at, and any file root later reads.
-
-**It has an escape that must be closed: a process the session asks to start something.** The flag passes
-only from parent to child. If the agent account has a running user manager, `systemd-run --user` starts a
-command as a child of that manager, which root started without the flag. The same holds for a cron or at
-daemon, for D-Bus activation on a session bus, and for a terminal multiplexer server started before the
-confined session. Either the account has none of these, or each is itself started with the flag (a drop-in
-on the account's `user@` unit can set `NoNewPrivileges=yes`; *to measure*).
-
-### 2.2 A confined view of the files
-
-| | Service manager unit (`ProtectSystem=strict`, `ProtectHome=tmpfs`, `BindPaths=`, `PrivateTmp=`, `TemporaryFileSystem=`, `InaccessiblePaths=`) | Landlock | bubblewrap |
-|---|---|---|---|
-| Kernel | mount namespaces, present on every kernel the mesh runs | an LSM, 5.13 or later, and enabled in the kernel's LSM list; features grow by ABI (5.19, 6.2, 6.7, 6.10, 6.12) — *to measure on each node* | user namespaces; refused where unprivileged user namespaces are switched off |
-| Who sets it up | root (a system unit, or `systemd-run` as root with `User=`); `claude-agent` already goes through the operator's sudo | the process itself, unprivileged, and it requires `no_new_privs` first | the process itself, unprivileged where user namespaces are allowed; sets `no_new_privs` itself |
-| Shape | hides or makes read-only whole trees; can show only a list of paths | an allow list of path rights, checked on access; nothing is hidden from a directory listing above an allowed path | a new mount tree built from bind mounts: only what is listed exists |
-| What the node-engine can read | the unit's properties from the service manager, and the session's processes' cgroup | `/proc//status` shows `NoNewPrivs`; whether a Landlock domain is applied is not readable from outside on older kernels — *to measure* | `NoNewPrivs` and the mount namespace in `/proc`; the bind list is not declared anywhere the engine reads |
-| Also gives | `RestrictSUIDSGID=`, `SystemCallFilter=`, `RestrictNamespaces=`, resource limits, one cgroup for every process of the session | per-process, stackable; can be added inside any of the other two | Claude Code's own sandbox on Linux is built on it |
-| Interactive terminal | `systemd-run --pty`; *to measure* with `use_pty` from the `sudo` module | unchanged | unchanged |
-
-**Claude Code's own sandbox** (from its documentation, *to measure* on the installed version): on Linux it
-runs the shell tool's commands, and their children, under bubblewrap with a write allow list and a network
-proxy. It does **not** cover the session's file tools (read, edit, write), which permission rules govern,
-nor the session's MCP servers. The shell tool also takes a per-call override that runs a command outside the
-sandbox, which a setting can forbid. So it confines the commands an agent types, not the session, and it is
-configured by the session's own settings. It is a useful inner layer; it is not the boundary ADR 0259 can
-rest on.
-
-**Composability.** The three nest: a unit started by root gives the outer view and `no_new_privs`; Landlock
-can narrow further inside it; Claude Code's bubblewrap runs inside both if the unit allows user namespaces
-(`RestrictNamespaces=` would forbid it, and with it a class of kernel exploits; a trade-off, see §5). The
-guard ([ADR 0245](../../02-DECISIONS/0245-a-verb-says-what-it-replaces-and-the-agent-is-guarded-from-working-round-the-mesh.md))
-runs inside the session as a hook and keeps refusing; the confinement makes a refusal it misses fail in the
-kernel. The tool runner runs outside, as the operator account, and the session reaches it over loopback, so
-the confinement must leave the loopback network open.
-
-**"Nothing else" is a short list, not nothing.** A program needs the dynamic linker's cache, the account
-database, the resolver file, the certificate store (the agent talks to its API over TLS), `/proc`, `/dev/null`
-and its terminal. Which `/etc` files the agent needs, and whether `/etc` is read-only or hidden but for them,
-is to be listed by trying it.
-
-### 2.3 Everything else through the mesh's tools
-
-Measured on 2026-10-10 with `mesh_overview` and `mesh_search`: every node holds `node-service-manager`, whose
-`journal` verb reads one unit's journal, and `node-hostname`, whose `entries` verb reads the machine's `/etc/hosts`.
-**No seat reads an arbitrary file, lists a directory or reads a log file that is not a unit's journal.**
-Today that is done through `node-login-shell.execute`, which runs any command as the operator account, and
-which the control-node withholds ([ADR 0268](../../02-DECISIONS/0268-the-login-shells-execute-is-withheld-on-the-control-node-until-a-call-to-it-needs-a-persons-approval.md)).
-
-A node filesystem seat cannot simply read a caller's path: the tool runner acts for any caller, and ADR 0266
-§7 calls a tool that reads a caller's path as the operator or as root a defect. A shape that would hold
-follows the store's reader after issue 337: it reads only what a module **declares readable** (its
-configuration, its logs), never a secret, through no symbolic link. Verbs it needs, at least: read a declared
-file, list a declared directory, read a declared log's tail with a filter.
-
-On the operator's machines the agent works in the operator's desktop and files (ADR 0266 §1). Moving it to an
-agent account there is only possible once such tools, and the desk's own tools, cover that work, and issue
-344 gaps 3 and 4 (the display, the chat client) need the agent account to have no access to the operator's
-display at all. Confining the agent while it **stays** the operator account does not hold: it can write a
-file the operator later runs with sudo (a shell start file), and the operator's own user manager starts
-anything without the flag.
-
-## 3. What must then be proven for root-free
-
-A confined start proves nothing if another way into the account exists. The proof becomes two halves.
-
-**The doors (read from the machine's configuration, cheap).**
-
-1. The account has no password: a locked entry in the shadow database.
-2. No ssh key logs into it: no `authorized_keys` under its home, or the ssh daemon refuses the account.
-3. No cron table and no at job for it, or no cron or at daemon runs.
-4. No lingering user manager, and no user units under its home; or its `user@` unit carries
- `NoNewPrivileges=yes` and the same confinement.
-5. Sudo rules only **toward** it (the operator to the agent account, through `claude-agent`), none from it.
- ADR 0266 §3 item 3 already judges the second half.
-6. `claude-agent` starts the session as the confined unit, and the unit's declaration is the mesh's.
-
-**The session itself (read from `/proc`, cheap, and closer to the truth than the doors).**
-
-7. Every process running as the agent account has `NoNewPrivs: 1` in its status and sits in the cgroup of
- a confined session unit. A process that does not is a door nobody listed, found by its effect.
-
-**The daemons that still act on request** (ADR 0266 §3 items 5 to 9 and part of the *not judged* list) stay
-judged as today, because `no_new_privs` does not reach them. The confined view can also hide their entry
-points: the container runtime's socket and the system bus can be made inaccessible to the session, which a
-unit property states and the engine reads.
-
-Every one of these reads is bounded by the number of processes and a handful of files, not by the size of the
-disks. Item 7 is checked at a moment; a process started between two looks is seen at the next one, within the
-engine's interval.
-
-**What becomes of the setuid search.** Three choices, for the operator (Q2):
-
-- **Retired for agent accounts** once 1 to 7 hold: it judges nothing a confined session can use.
-- **Kept as defence in depth**, resumable and unbounded, with a finding raising a warning but no longer
- gating `root-free`.
-- **Kept only for accounts that are not confined**: the operator account where agents still run as it, and
- any account whose processes fail item 7.
-
-## 4. What this does not cover
-
-- **The mesh MCP server has no login of its own** (ADR 0144, ADR 0152: the machine's login is the authority
- on loopback; ADR 0266 §7: the tool runner acts for any caller). A confined agent reaches the same tools as
- the operator. Every tool that runs a caller's command or reads a caller's path as another account remains a
- way round, as ADR 0266 §7 and issue 344 gaps 2, 5 and 11 already record. Research
- [039](../039-a-tool-that-needs-a-persons-approval/00-overview.md) is the work that narrows it.
-- **A kernel exploit.** `no_new_privs` and a confined view do not stop a flaw in the kernel. A system call
- filter and no new user namespaces shrink the surface; the second conflicts with Claude Code's bubblewrap
- sandbox (§2.2). The residual risk is the kernel's, as for every other process on the machine.
-- **Network reach.** The session keeps the network it needs for its API and the loopback MCP server; a
- network allow list is a separate question.
-
-## 5. Open questions for the operator
-
-1. **Is the confined start the boundary ADR 0259 rests on**, replacing the setuid search as the gate for
- `root-free` on a node with an agent account? Recommendation: yes, because it answers "can the session
- become root" instead of "does a program exist", and its cost does not grow with the data on the machine.
-2. **What becomes of the setuid search** (§3)? Recommendation: keep it resumable as defence in depth for a
- warning, gate `root-free` only on unconfined accounts.
-3. **Which mechanism carries the confinement?** Recommendation: a service manager unit started by root
- through `claude-agent`, because it needs no new package, gives `no_new_privs` and the view in one
- declaration the node-engine can read; Landlock as an optional inner layer where the kernel has it; Claude
- Code's bubblewrap sandbox kept for the shell tool, with its per-call override forbidden.
-4. **May the session keep user namespaces**, so that Claude Code's sandbox works inside it, at the price of
- a larger kernel surface? Or is the outer unit enough and the inner sandbox dropped?
-5. **Is a node filesystem seat that reads only declared paths enough** for what agents do on the
- control-node, and is it the precondition for moving the operator's machines to an agent account?
-
-## 6. Next measurements
-
-- On each node: the kernel's LSM list and Landlock ABI, whether unprivileged user namespaces are allowed, and
- whether `sudo -iu` there starts a user manager for the target account. No mesh tool reads the first two
- today; that is itself a gap for the node-engine's facts.
-- A trial `claude-agent` as a transient unit on a non-control node: which `/etc` files the session needs, that
- `sudo -n true` fails with the flag set, that `systemd-run --user` is refused or inherits the flag, and that
- the mesh MCP server and the guard still answer.
-- The `/proc` sweep of item 7 on the control-node: how many processes run as the agent account, and how long
- the read takes.
diff --git a/01-RESEARCH/042-the-mesh-keeps-its-own-issues/00-overview.md b/01-RESEARCH/042-the-mesh-keeps-its-own-issues/00-overview.md
deleted file mode 100644
index b4272350..00000000
--- a/01-RESEARCH/042-the-mesh-keeps-its-own-issues/00-overview.md
+++ /dev/null
@@ -1,86 +0,0 @@
----
-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.
diff --git a/01-RESEARCH/042-the-mesh-keeps-its-own-issues/01-findings.md b/01-RESEARCH/042-the-mesh-keeps-its-own-issues/01-findings.md
deleted file mode 100644
index e14351a7..00000000
--- a/01-RESEARCH/042-the-mesh-keeps-its-own-issues/01-findings.md
+++ /dev/null
@@ -1,259 +0,0 @@
-# 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.` (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 |
diff --git a/01-RESEARCH/044-what-leaves-the-controller-next/00-overview.md b/01-RESEARCH/044-what-leaves-the-controller-next/00-overview.md
deleted file mode 100644
index c0a75935..00000000
--- a/01-RESEARCH/044-what-leaves-the-controller-next/00-overview.md
+++ /dev/null
@@ -1,122 +0,0 @@
----
-status: active
-initiated: 2026-10-10
-touches:
- - 03-DESIGN/01-to-be/06-the-controller.md
- - 03-DESIGN/01-to-be/45-a-core-that-cannot-fail-silently.md
- - 03-DESIGN/01-to-be/46-the-conversation-with-the-operator.md
- - 02-DECISIONS/0008-a-context-owns-its-store.md
- - 02-DECISIONS/0153-the-record-is-read-by-a-module-and-the-console-lists-it.md
- - 02-DECISIONS/0239-a-delivery-is-owned-by-the-mesh-delivery-module-and-runs-from-commit-to-delivered.md
- - 02-DECISIONS/0244-the-mesh-is-described-in-domains-and-one-word-names-one-thing.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
- - 02-DECISIONS/0276-merges-are-assembled-in-a-rolling-window-and-each-batch-is-delivered-by-one-walk-with-one-commit-per-repository.md
- - the controller's code and the walk of its builds
-became: []
----
-
-# 044 — What leaves the controller next
-
-## What
-
-Which part of the controller should move into a module of its own next, chosen by evidence rather
-than by taste: the domains whose code changes most often and needs the core least. The effort frames
-the questions below and answers none of them yet; the research itself is done separately.
-
-The controller has shed parts before. Deliveries moved to the `mesh-delivery` module
-([ADR 0239](../../02-DECISIONS/0239-a-delivery-is-owned-by-the-mesh-delivery-module-and-runs-from-commit-to-delivered.md)),
-issues to the issue tracker held by `mesh-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)),
-and reading the record to the `records` module
-([ADR 0153](../../02-DECISIONS/0153-the-record-is-read-by-a-module-and-the-console-lists-it.md)).
-This effort asks what comes after those three, and whether the way they moved is a pattern worth
-repeating.
-
-## Why
-
-The controller is the component that changes most and costs most to change. Measured on the
-controller's `main` branch on 2026-10-10:
-
-- 191 merges in the 7 days before.
-- About 104,000 lines of Go outside tests. About 44,000 of them sit in one package (`cmd/mesh-controller`,
- 94 files), which took about 60% of the week's file changes. The next packages by share of change were
- `internal/catalogue`, `internal/inventory`, `internal/broker` and `internal/link`.
-
-Every controller change, however small, walks the core's tier through the first-node gate, and only one
-walk is open at a time
-([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)).
-Issue 382 in the issue tracker (a small fix takes 45 minutes to reach a node) measured the cost on
-2026-10-10: a notification fix merged at 14:46 had not reached its node at 15:08, because it waited
-behind two other walks, and each tier costs about 4 to 5 minutes. A change to a part that does not
-need to be in the controller still pays that price, and still holds up every other walk while it does.
-
-## The operator's direction (2026-10-10)
-
-The operator holds that the controller should not be the mesh's monitor, and proposes a module of its
-own for it: a **mesh-monitor** module claiming a seat for watching the mesh. This is the effort's leading
-hypothesis, to be tested by the questions below rather than assumed by them. What it would hold:
-
-- the probes (resolvers answering, version drift, the machine's network checks);
-- the bus's advisories, read live, including JetStream's API errors with the user, subject and code that
- caused each (issue 328 waits for this module rather than adding a reader to the controller);
-- conditions: raised, cleared, silenced and handed to the operator's channels;
-- the hand-act log and its *healer wanted* counts;
-- what a bundle-only module cannot raise today
- ([ADR 0240](../../02-DECISIONS/0240-a-module-says-how-it-is-healthy-and-the-node-engine-judges-it.md) rule 8), such as the
- issue tracker's fix stuck after its merge
- ([ADR 0281](../../02-DECISIONS/0281-an-issue-knows-whether-its-fix-is-delivered-and-a-fix-stuck-after-its-merge-is-loud.md)): a module reports it on
- the bus and the monitor raises it.
-
-What it would gain: a change to watching no longer walks the controller's tier, and the mesh keeps
-watching while the controller is down. What it must answer: who notices when the monitor itself is
-down (the controller's liveness check of every module is the candidate, the one piece of watching the
-controller keeps), and how conditions move from the controller's store to the monitor's without a gap
-(question 5).
-
-## The questions
-
-1. **Where the change goes, by domain.** Group the last month's controller merges by the domains of
- [ADR 0244](../../02-DECISIONS/0244-the-mesh-is-described-in-domains-and-one-word-names-one-thing.md)
- (Module, Core, Placement, Provisioning, Identity and access, Change and delivery, Health and repair,
- Data, Connectivity, Operator and conversation). How is a merge assigned to a domain when it touches
- several, and how many merges cannot be assigned at all? Is the domain of a file, a package or a verb
- the better unit?
-2. **Which domains are worth moving.** Rank the domains by their rate of change multiplied by how little
- they need the core: how few of the controller's stores they read, how few of its verbs they call
- in-process, and whether they could do their work through the bus alone. Which signal of "needs the
- core" can be measured from the code, and which only by trying?
-3. **The candidates to test against that ranking.**
- - The probes, conditions and the hand-act log as a monitoring module (to-be 45), raising and
- clearing conditions on the bus.
- - The asks, proposals and the wording of notifications, moved to the operator's channel side
- (to-be 46).
- - The machine's network checks
- ([ADR 0241](../../02-DECISIONS/0241-a-machine-says-how-its-network-is-and-an-outside-writer-of-a-mesh-file-is-a-finding.md)).
-
- For each one: what it reads from the controller today, what it writes there, and what would break
- if the controller were not reachable while it ran.
-4. **What must stay.** Deciding what each node runs is the controller's reason to exist (to-be 06).
- Where exactly does that boundary run: does the catalogue's reading, the inventory, the broker or the
- link belong inside it or beside it? What is the smallest controller that still decides every node?
-5. **How a domain moves out without a flag day.** How did the earlier three move: served in both places
- for a while, a seat verb promised before it is required
- ([ADR 0246](../../02-DECISIONS/0246-a-seats-new-verb-is-promised-before-it-is-required.md)), a store
- handed over ([ADR 0008](../../02-DECISIONS/0008-a-context-owns-its-store.md))? What went wrong in those
- moves, and what does a move need so that no node and no caller sees a gap?
-6. **How the effect is measured afterwards.** Which numbers say a move worked: controller merges per week,
- the time from merge to a node running the fix, the share of walks that touch the core's tier, the
- number of walks waiting behind a controller walk. What is their baseline today, and where are they
- read from, so that the same measurement can be repeated after each move?
-
-## Words
-
-The glossary has no word for "how little a domain needs the core". This effort uses **coupling to the
-core** as a plain description until a decision settles a word. "Monitoring module" is also a plain
-description, not a glossary word; the glossary's words for what it would hold are **probe**,
-**condition** and **hand-act**.
-
-## What it touches
-
-The controller's design and code (to-be 06), the core that cannot fail silently (to-be 45), the
-conversation with the operator (to-be 46), the walk of builds (ADR 0276), and the earlier extractions
-(ADR 0153, ADR 0239, ADR 0275) as precedent.
diff --git a/01-RESEARCH/045-a-knowledge-base-the-mesh-writes/00-overview.md b/01-RESEARCH/045-a-knowledge-base-the-mesh-writes/00-overview.md
deleted file mode 100644
index 8025d394..00000000
--- a/01-RESEARCH/045-a-knowledge-base-the-mesh-writes/00-overview.md
+++ /dev/null
@@ -1,112 +0,0 @@
----
-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.
diff --git a/01-RESEARCH/045-a-knowledge-base-the-mesh-writes/01-findings.md b/01-RESEARCH/045-a-knowledge-base-the-mesh-writes/01-findings.md
deleted file mode 100644
index dacb0bfd..00000000
--- a/01-RESEARCH/045-a-knowledge-base-the-mesh-writes/01-findings.md
+++ /dev/null
@@ -1,295 +0,0 @@
-# 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.
diff --git a/01-RESEARCH/README.md b/01-RESEARCH/README.md
index 0f82c9b8..a7ec1bfc 100644
--- a/01-RESEARCH/README.md
+++ b/01-RESEARCH/README.md
@@ -1,46 +1,12 @@
# 01-RESEARCH
-Investigations that have not yet hardened into design.
-
-## Structure
-
-Each effort lives in `NNN-descriptive-name/` and **must** contain `00-overview.md`, carrying its
-state in YAML frontmatter and a prose summary below it:
-
-```yaml
----
-status: active | graduated | abandoned
-initiated: YYYY-MM-DD
-touches: [] # design docs, subsystems or areas the effort bears on
-became: [] # required when status is terminal — what it turned into
----
-```
-
-The prose says what is being investigated, why, and what it touches. It does not restate the
-status — status lives in one place, and two places is one too many.
-
-Further documents in the same folder hold the work itself: notes, evidence, option analyses,
-draft designs.
-
-## Lifecycle
-
-| status | Meaning |
-|---|---|
-| `active` | Investigation in progress. |
-| `graduated` | Checked against `00-META`, decided in `02-DECISIONS/`, and specified in `03-DESIGN` — see `became:`. |
-| `abandoned` | Stopped or superseded. Nothing is deleted. |
-
-An effort graduates by producing a decision record **and** a `03-DESIGN` entry. It is abandoned
-in place — never deleted. What was rejected, and why, is the more expensive half to
-rediscover.
-
-Starting and closing efforts is playbook territory:
-[`00-META/process/01-research.md`](../00-META/process/01-research.md) and
-[`02-graduation.md`](../00-META/process/02-graduation.md).
-
-## Rules
-
-- Markdown only. Do not skip or reuse a sequence number.
-- **Evidence, not assertion.** An effort that measured nothing has not finished.
-- Research describes real observations but **never identifies the mesh it observed**. The shape
- of a finding survives anonymisation intact.
+**The research efforts live in the knowledge base, not here** (ADR 0290). The knowledge base is the module
+`mesh-knowledge`, holding the seat `knowledge-base`; it holds every effort whole — its number, title, status,
+the day it began, its documents, its links and every version — and it is the only place an effort is read.
+This repository keeps no copy. From any node and any folder, through the mesh MCP server:
+`knowledge-base.ask` with a question in plain words, or `knowledge-base.get` with `kind: research` and a
+number (`reasoning: true`, or `section` with a document's name, for its documents). An effort is started
+with `knowledge-base.reserve` and `knowledge-base.propose`, as playbook
+[01](../00-META/process/01-research.md) says, and closed through playbook
+[02](../00-META/process/02-graduation.md). The efforts left this folder at hq commit f08920199d25, which
+the knowledge base's import read (issue 426).
diff --git a/02-DECISIONS/0001-mesh-brokers-nodes-host-agents-think.md b/02-DECISIONS/0001-mesh-brokers-nodes-host-agents-think.md
deleted file mode 100644
index 546301ec..00000000
--- a/02-DECISIONS/0001-mesh-brokers-nodes-host-agents-think.md
+++ /dev/null
@@ -1,235 +0,0 @@
----
-topic: the mesh
-status: accepted
-date: 2026-08-22
-deciders: jochen
-reconstructed: false
----
-
-# 1. The mesh brokers capabilities; nodes host; agents think
-
-## Context
-
-HAL has 124 modules. The count is not the problem — it is the symptom. Modules are split
-because splitting is the only granularity the platform offers, and domains are merged
-because a shared database is the only integration it offers. Both pressures push in the
-same direction: boundaries end up drawn by deployment accident rather than by domain.
-
-Three observations establish the state.
-
-**The word "agent" means two different things.** The original design treated a node as an
-agent with thinking abilities. Later, noxflow implemented agents as employees with
-skills, workspaces and tasks. Both survive. The collision is visible in the data — there
-are **two agent rows per node**:
-
-| agent | skills |
-|---|---|
-| one named after the node | `{deploy,verify,operate}` |
-| one named `hal-` | `{}` |
-
-One carries the work; the other carries only identity, existing to hold a licence for
-HAL's own sessions. The same split appears in the schema: `nodes.hal_claude_account` and
-`agents.claude_account` are one fact in two tables in two databases, and what was
-documented as "three licence touchpoints" is one concept modelled three times.
-
-**Domains integrate by sharing a schema.** `noxflow` is 45 tables spanning five domains —
-tasks (7), agents (13), knowledge (8), meetings (7), scheduling (2). Its original purpose
-is 7 of 45. Because agent identity lives in noxflow's database, work that belongs
-elsewhere must be implemented there: per-agent Claude credentials had to be written by
-the noxflow runtime, even though node identity — the same kind of fact — lives in the
-mesh registry.
-
-**The core domain has no context to live in.** The mesh's distinguishing feature is that
-a module declares `requires: postgres/database` and never learns where the database
-lives, who owns the credential, or how it rotates. That is capability brokering, and it
-is what makes this a mesh rather than four machines with a configuration manager. Yet the
-logic implementing it sits in `modules/postgres/tools/index.ts` — a provider module,
-where "the mesh brokers credentials" cannot be expressed. On 2026-08-22 three of its
-invariants were found violated simultaneously (see Consequences).
-
-## Considered Options
-
-1. **Keep the current layout; fix bugs as they surface.** Rejected. The faults are not
- independent. Every incident on 2026-08-22 — credentials written by the wrong module, a
- dependency question answered wrongly twice, an invariant enforced nowhere — traced to
- a boundary that was never stated. Fixing them individually leaves the generator intact.
-
-2. **Merge aggressively into few large modules.** Fewer names, same problem: a shared
- schema across domains is what produced noxflow, and doing it deliberately would
- produce it again at larger scale.
-
-3. **Decompose by bounded context, with the mesh as a broker.** Name contexts after their
- aggregates, integrate through a published record rather than a shared schema, and let
- deployment granularity be a feature-level concern rather than a reason to create a
- module. **Adopted.**
-
-## Decision
-
-### The domain, in one sentence
-
-**The mesh brokers capabilities. Nodes are places where work runs. Agents are personas
-that think and act.** Everything else supports one of those three.
-
-### What this is, plainly — and what "mesh" does not mean
-
-*Written 2026-08-29, from working through connectivity and asking whether the word still fits.*
-
-Four layers. Naming them honestly is worth more than the word on the tin:
-
-| | |
-|---|---|
-| **machines are linked by a private network** | and every machine reaches every other over it |
-| **one node holds knowledge of all of them** | the control plane, and only it |
-| **modules are how anything is built and delivered** | this *is* the CI/CD, not something beside it ([ADR 0010](0010-delivery.md)) |
-| **every node runs a session you can message** | a feature of the node, remembering across callers; any node can message any other ([ADR 0004](0004-a-node-and-how-it-joins.md)) |
-| **workers are hired onto nodes to do tasks** | employees, with a lifecycle — a different thing from the row above ([ADR 0003](0003-agents-are-persistent-employees.md)) |
-
-**The last two rows are not the same thing and the vocabulary of one does not describe the other.**
-A node's session comes with the machine: nobody hires it, it holds no tasks, it is never
-reassigned, and it goes when the node leaves. A worker is an employee — named, hired, drained,
-retired, movable. They are built from the same parts and run on entirely different terms, and
-collapsing them is how the employee vocabulary ends up stretched over something it does not fit.
-
-**Neither makes the node itself a thinking thing.** A node is a machine; both of these run *on*
-one, which is why *a node does not authenticate to a model provider, agents do* is unaffected by
-either.
-
-**Where the value is** is that both can reach across the whole set: a shell, a service, a file, or
-simply a question to another node. Not machines that can be configured centrally, which is
-ordinary, but a set of machines that can be worked across as though they were one.
-
-**This is not a mesh in the peer-to-peer sense and will not become one.** The word describes what
-machines can reach, not how they are governed:
-
-| | a mesh? |
-|---|---|
-| what a machine can reach | **yes** — genuinely any to any |
-| how the traffic travels | no — anything crossing sites transits the hub |
-| who decides | no. One node, declared |
-
-**And *master* overstates it in the other direction.** A master implies the others need it in
-order to function. They do not: every node holds what it was last told and runs from that copy
-*always* — not as a fallback, as the only mode it has. So the control plane being gone is every
-node in the ordinary disconnected situation at once, and **what is lost is change, not
-operation.**
-
-The accurate phrase is **one authority, no failover**, and both halves are deliberate
-([ADR 0006](0006-the-substrate-and-the-control-plane.md)).
-
-### Nodes and agents are decoupled
-
-A node is a place where an agent can run — that is the entire relationship. There is no
-resident agent, no node-owned identity, no ownership in either direction. Agents named
-after a node remain, as **ordinary agents** that happen to hold infra skills.
-
-Consequently `nodes.node_license` and `nodes.hal_claude_account` cease to exist: a node
-does not authenticate to a model provider, agents do. The two agent rows per node merge.
-
-### There is one kind of participant, and some are human
-
-Human and non-human participants are both **agents**. Both hold identity and credentials;
-both act, remember and coordinate. What differs is **modality** — how an agent acts:
-
-| modality | credential is delivered to |
-|---|---|
-| spawned session | that agent's own config directory |
-| shell or desktop | that agent's home on the node it acts from |
-
-A node holds no licence. **An agent holds credentials, and delivery follows that agent's
-node bindings and modality.** A human agent's grant arrives in the home directory of the
-user it acts as, on the nodes it is bound to — the same rule that puts a spawned agent's
-grant in its config directory, with a different target.
-
-This requires one fact the mesh does not record today: which user, on which node, a given
-human agent acts as. Adding it is what removes the node licence — the node is currently
-standing in for an identity the mesh cannot name. It is also what makes a second human
-agent require no new mechanism.
-
-### Contexts
-
-| context | aggregate | subdomain |
-|---|---|---|
-| `hal/mesh` | **Provision**, Node, Module — brokering and its bookkeeping | core |
-| `hal/agents` | **Agent** — identity, licence, runs, memory, thoughts | core |
-| `hal/work` | **Task** — workflows, bindings | core |
-| `hal/stream` | **Thread** — mentions, messages, meetings, notifications | core |
-| `hal/delivery` | **Pipeline** — jobs, artifacts, features | supporting |
-| `hal/knowledge` | **Document** — spaces, revisions, review | supporting |
-| `hal/ai` | **Licence** — provider grants and rotation | supporting |
-| `hal/observability` | **Check** | supporting |
-| `hal/config` | **Setting** — env, secrets, PKI | generic |
-
-A *brain* — memory, thoughts, cognition — is a concept the Agent aggregate owns. It is
-not a module. Anatomy makes attractive names and poor boundaries; today `hal/brain` names
-infrastructure and `hal/cortex` describes itself as messaging while running nowhere.
-
-### Provisioning is the core domain, not plumbing
-
-`hal/mesh` is a broker; the registry is its bookkeeping. Its invariants are explicit and
-owned:
-
-- one rotation source per resource
-- a credential change fans out to every consumer
-- a consumer never holds a credential the provider does not know about
-
-### Contexts integrate through the record, never a shared schema
-
-`hal/stream` is the published language. A context publishes; it does not join across a
-boundary. This is what dissolves "meetings" as a domain — a meeting is a thread, and a
-notification is a mention not yet read.
-
-### Third-party software leaves the repository
-
-`plex`, `sonarr`, `postgres`, `verdaccio`, `docker-registry` and 88 others run **on** the
-mesh; they are not **of** it. The pipeline and provisioning are deliberately
-module-agnostic, so HAL's own modules dogfood exactly what external modules use — which
-is what makes the separation safe rather than merely tidy.
-
-## Consequences
-
-**The invariants now have an owner, and were measurably unowned before.** On 2026-08-22,
-`provision_ensure` — documented as "NEVER rotates an existing secret" — was found to mint
-a new password on every adoption and update only the provider's row. `hal_notifications`
-consumers on three nodes held dead credentials for two days; `hal_transcripts` had two
-rows written 216 ms apart, so at most one could match the live role.
-
-**noxflow dissolves.** `hal/work` inherits tasks and workflows — the concept it was built
-for. Agents, knowledge, meetings and scheduling return to their contexts. The name goes
-away.
-
-**`hal/sdk` shrinks.** 155 files, 34,636 lines, containing code from every context —
-including `workflow-engine.ts` and `task-commands.ts`, work-domain logic in the kernel
-every module imports. Each landed there to avoid a cycle between modules that both needed
-it; a domain module can only own its shared code once the domain has a module. Extraction
-is therefore downstream of this decision, not independent of it.
-
-**Two mechanisms are prerequisites, not follow-ups.**
-
-- *Named features with per-node opt-in.* Without it, every independently deployable unit
- inside a context becomes a module again and the count returns. `hal/claude-licences`
- exists solely because one daemon must run on one node.
-- *A local mesh in containers.* `dev_up` starts providers "via systemctl (same as
- production)" — it borrows the host, and no mesh can be stood up locally. Everything that
- manifests **between** nodes is therefore discoverable only in production, which is where
- every fault of 2026-08-22 was found. A refactor of this size is otherwise unverifiable.
-
-**Credential delivery becomes uniform.** Per-agent credential directories, built for
-spawned sessions, extend to human agents unchanged. A file previously scoped to a node
-becomes scoped to an agent — which would have prevented the class of failure where a
-rotation reached one node of four while the mesh reported success.
-
-**Migration is incremental and long.** Contexts can be extracted one at a time behind the
-existing pipeline. Nothing here requires a flag day, and nothing here is cheap.
-
-## References
-
-- [`01-RESEARCH/001-module-domain-decomposition`](../01-RESEARCH/001-module-domain-decomposition/analysis.md)
- — current-state evidence, table counts, open questions
-- [`00-META/how-we-build.md`](../00-META/how-we-build.md) — naming and integration rules
-- `modules/hal/sdk/src/feature-handlers/index.ts` — `FEATURE_HANDLERS`, the fixed handler
- array that makes a feature a singleton per module
-- `modules/postgres/tools/index.ts` — the adoption path that rotates a shared credential
-- Mediahuis `papa-hq`, ADR 0020 *Composable, independently-shippable modules* — the
- constraints that make a unit independently shippable, applicable unchanged to features
-- impire.io / soulstream — *the record* as integration substrate, personas over services,
- and "cheap awareness and expensive thinking"
diff --git a/02-DECISIONS/0002-nodes-communicate-over-a-broker.md b/02-DECISIONS/0002-nodes-communicate-over-a-broker.md
deleted file mode 100644
index b548c9ba..00000000
--- a/02-DECISIONS/0002-nodes-communicate-over-a-broker.md
+++ /dev/null
@@ -1,69 +0,0 @@
----
-topic: the mesh
-status: accepted
-date: 2026-02-25
-deciders: jochen
-reconstructed: true
----
-
-# 2. Nodes communicate over a message broker, not over HTTP
-
-> Reconstructed after the fact from the evidence cited below. The decision was taken in
-> implementation, not in a record; this document states what was decided and why, not a
-> deliberation that happened.
-
-## Context
-
-The mesh is a set of machines that must call each other's capabilities. On the day the
-repository was founded there was no inter-node transport at all — each node was configured
-independently and shared nothing at runtime.
-
-Three properties were required and are visible in everything built since:
-
-- A node behind a household NAT must participate fully. It can dial out; nothing can dial in.
-- A node that is asleep, rebooting or upgrading must not cause a caller to fail — the request
- should wait, not error.
-- Adding a node must not require editing anything on the nodes that already exist.
-
-## Considered options
-
-1. **HTTP APIs between nodes.** Rejected. Every node becomes a server that every other node
- must be able to reach, which the NAT case makes impossible without inbound tunnels to each
- participant. It also makes node liveness a caller's problem: a request to a sleeping node
- is an error rather than a wait.
-2. **Polling a shared database.** Rejected. Latency is the poll interval, load is constant and
- independent of demand, and request/reply has to be built on top of it by hand.
-3. **A central message broker with per-node exchanges.** Chosen.
-
-## Decision
-
-All inter-node communication goes through a message broker. Every node owns a topic exchange
-named for itself and a request queue; a shared mesh exchange carries commands and events that
-are not addressed to one node.
-
-Three message shapes, and only three:
-
-- **RPC** — request/reply, for calling a capability that lives on another node.
-- **Commands** — instructions to do a stage of work, addressed by what is to be done.
-- **Events** — statements that something happened, addressed to nobody.
-
-Every node dials the broker outbound. Nothing dials a node.
-
-## Consequences
-
-- NAT stops being an architectural concern. A node's reachability is a property of the broker
- connection, not of its network position.
-- A call to a node that is down waits in that node's queue instead of failing. This is usually
- right and occasionally the wrong thing entirely — a queued command for a node that never
- returns is a stall with no error, which is the failure shape this mesh keeps rediscovering.
-- The broker is a single point of failure and a single point of trust. Its credential is
- mesh-wide, so rotating it is a mesh-wide operation.
-- Tools never leave the host: a remote call proxies over the broker and the credentials stay
- where the capability is.
-
-## References
-
-- The broker was stood up on 2026-02-25, the second day of the repository.
-- Knowledge base: `mesh` (transport, exchanges, queue naming), `troubleshooting/amqp-credential-rotation`.
-- The stall shape is recorded in `troubleshooting/empty-pipeline-blocks-the-queue` and
- `troubleshooting/daemon-and-tool-server-share-a-request-queue`.
diff --git a/02-DECISIONS/0003-agents-are-persistent-employees.md b/02-DECISIONS/0003-agents-are-persistent-employees.md
deleted file mode 100644
index 5bbe516c..00000000
--- a/02-DECISIONS/0003-agents-are-persistent-employees.md
+++ /dev/null
@@ -1,75 +0,0 @@
----
-topic: the mesh
-status: accepted
-date: 2026-07-12
-deciders: jochen
-reconstructed: true
----
-
-# 3. An agent is a persistent employee, not an instance of a pool
-
-> Reconstructed after the fact from the evidence cited below.
-
-## Context
-
-Agents were originally a **pool**: a named kind of worker, scaled to some number of
-interchangeable instances. Work went to whichever instance was free.
-
-That model has no place to put the things that turn out to matter. An agent that accumulates
-knowledge of a domain cannot keep it, because the next task lands on a different instance. An
-agent cannot own a workspace, because there are several of it. It cannot be held to a policy —
-warned for a violation, then dismissed — because there is no continuing subject to warn.
-
-Scaling was also solving a problem the mesh does not have. Instances were being multiplied to
-get concurrency, when concurrency is a property of how much work one agent may hold at once.
-
-## Considered options
-
-1. **Keep the pool, attach memory to the pool.** Rejected: shared memory across
- interchangeable workers is a knowledge base, not an agent's experience, and the mesh
- already has one.
-2. **Keep the pool, make instances sticky.** Rejected as a pool pretending to be identities —
- identity by scheduling accident, lost on any restart.
-3. **One agent is one persistent identity, with concurrency as a property of it.** Chosen.
-
-## Decision
-
-An agent is a **singular, named, persistent identity**: a home node, a workspace on that node,
-accumulating memory, and a lifecycle — hired, active, draining, retired. Not a pool member.
-
-Concurrency is a property of the agent, not a count of copies: an agent has a cap on how many
-sessions it may hold at once.
-
-Lifecycle is explicit and has verbs. An agent is hired onto a node; it may be reassigned while
-idle; it is retired by draining first, and forced only deliberately. Retired agents are not
-deleted.
-
-Surge capacity is expressed within the model rather than against it: a template agent is a
-blueprint, cloned into a real agent with a lifetime when a queue grows, drained and retired
-when it expires. A temporary employee is still an employee.
-
-Some agents are **human**. What differs is modality — how the agent acts — not category. A node
-itself is an agent of a kind exempt from the hiring lifecycle.
-
-## Consequences
-
-- Memory, workspace and reputation have a subject to belong to. Policy becomes possible: an
- agent that violates a rule can be warned, and warned agents can be dismissed.
-- The mesh gained a hiring model, and with it the question of who may hire.
-- Scaling by adding instances is gone. If one agent is saturated, either its session cap rises
- or another agent is hired — both deliberate acts.
-- The transition was not free. Lifecycle columns had to reach every query that selects an
- agent, and the ones that were missed failed at the moment of hiring rather than at startup.
-- This is the decision [ADR 0001](0001-mesh-brokers-nodes-host-agents-think.md) generalises:
- one kind of participant, differing only in modality.
-
-## References
-
-- `docs(adr): agents as persistent employees + MINERVA librarian` (#495), 2026-07-12 — the
- original record, in the code repository.
-- `feat(B4): one persistent employee, N sessions — rename max_instances → max_sessions` (#547)
- and `feat(noxflow): B3 — workspace provisioner for agent employee model` (#549), 2026-07-20.
-- `feat(noxflow): warn-then-fire agents who merge to main without review` (#209), 2026-06-01 —
- policy that presumes a continuing subject, predating the model that provides one.
-- Knowledge base: `agents/employee-lifecycle`, `agents/temp-surge`, `agents/workspace-layout`.
-- The migration cost: `troubleshooting`/`noxflow-agent-enriched-select-missing-lifecycle-columns` (#546).
diff --git a/02-DECISIONS/0004-a-node-and-how-it-joins.md b/02-DECISIONS/0004-a-node-and-how-it-joins.md
deleted file mode 100644
index a958a769..00000000
--- a/02-DECISIONS/0004-a-node-and-how-it-joins.md
+++ /dev/null
@@ -1,263 +0,0 @@
----
-topic: the tiers
-status: accepted
-date: 2026-08-28
-deciders: jochen
-reconstructed: false
----
-
-# 4. A node, and how it joins
-
-*Consolidated 2026-08-28 from four records.*
-
-## What a node is
-
-**A managed machine inside the mesh.** Not a device that is merely known about, not an
-unprivileged something. If the mesh does not manage it, it is not a node — it is a client, a
-peer, or a thing on the network, and those want their own names rather than a weakened version of
-this one.
-
-**A disconnected node is still a node, in a different situation.** Reachability is **state, not
-class**. A machine switched off, roaming, or behind a connection that has dropped has not become
-a lesser kind of thing; it has a last-known state and a pending set of declarations.
-
-The distinction people reach for is real, but it is **capability** — what this machine can be
-asked to do — and that belongs in the host's profile rather than in the definition of a node.
-
-**This is the rule that does the most work elsewhere.** A single control plane is tolerable
-because its absence is every node in the ordinary disconnected situation at once. An episodic
-host on a phone is that situation more often. Neither needed a new mechanism.
-
-### A node runs one agent session
-
-*Written 2026-08-29. It runs on every node today and appeared in no record, which is how something
-deliberate comes to look accidental.*
-
-**A node is a machine. The session is a feature of it** — one of the things running there, like the
-host, like any workload. The node does not think; something on the node does. Which is why
-[ADR 0001](0001-mesh-brokers-nodes-host-agents-think.md)'s *a node does not authenticate to a model
-provider, agents do* holds unchanged: the session authenticates, and it is not the machine.
-
-**It is permanent, and it remembers.** Anything in the mesh can send it a message; it replies; and
-what it was asked ten minutes ago is still there next week, alongside what everything else asked in
-between — the same way both sides of any conversation remember it.
-
-Its system prompt is the node's **engram** — what makes one node's replies recognisably its own
-rather than generic.
-
-**It has its own tools**, and fewer than a session a person is driving directly. So a question can
-be answered by going and looking: *what is in our forge*, not only *what is your battery*.
-
-**Messages travel the broker like everything else** ([ADR 0002](0002-nodes-communicate-over-a-broker.md)).
-There is no second transport and nothing is dialled.
-
-**Any node can message any node, and this is the one part of the system that is genuinely a mesh**
-— symmetric, with no centre. A node that is asked something it does not have can ask another, and
-how it passes the question on is its own business: it may say who wants to know, or simply ask. A
-person relaying a question makes the same choice, and it follows from the engram rather than from a
-message format.
-
-**There is no authorisation between nodes.** Every node is the operator's own, so a message from
-one is a message from them, and asking a node something is asking a colleague rather than
-presenting credentials. Stated once so it is not discovered later: **the mesh boundary is therefore
-the security boundary** — anything inside can reach what any node can reach, which is what puts the
-whole perimeter on the token and the overlay
-([ADR 0007](0007-connectivity.md)).
-
-**It can be switched off, and switched off it still answers.** A node whose session is disabled
-replies saying so, at once, with no model involved — the queue is still read, and the state is the
-reply. That is deliberate and it is the same rule the host follows about a service that does not
-exist: **absence must never be indistinguishable from a failure to answer.** A node with nothing
-there is a silence somebody has to go and diagnose; a node that says *I am switched off* is not.
-
-**One per node, always, and it cannot be moved to another machine.** Two and nothing decides which
-replies; none and the node is mute; moved, and one machine is answering as another.
-
-**It is not an employee** ([ADR 0003](0003-agents-are-persistent-employees.md)). Nobody hires it,
-it holds no tasks, it drains nothing and it is never reassigned — that vocabulary was written for
-workers and does not describe this. It exists because the node does, and it is gone when the node
-leaves.
-
-## How it joins
-
-**The host has one behaviour and two sources of declaration.** What differs between the first
-node and the fiftieth is not what the host does but where the declaration comes from — and, as
-above, that is a situation rather than a class.
-
-| | declaration comes from |
-|---|---|
-| no mesh reachable | the pinned bundle the host carries |
-| mesh reachable | the control plane, over the link |
-
-**The first node is not a different kind of node.** It is a node whose mesh is not up yet. It
-applies the bundle it carries, the control plane comes up on top of it, and from that moment it
-takes declarations like everything else. **Its specialness is temporary and self-erasing**, which
-is what the hand-run bootstrap scripts never were.
-
-**A joining node does the minimum to be reachable and nothing else** — an identity, an address,
-and one peer to reach. It does **not** compute the overlay: the whole peer set is derived
-centrally and pushed down.
-
-That is also why the migration is smaller than it looked. The hard part of the overlay — every
-node's key, address, site and reachability — is only needed to compute the *whole* mesh, and a
-joining node needs one peer.
-
-## The link is the security boundary
-
-**Everything reaching a node arrives one way**, and four properties make that a boundary rather
-than a pipe.
-
-**It is outbound and node-initiated.** The node dials the control plane; nothing dials a node. Not
-only defensive — most nodes sit behind a household connection with no forwarded port, so an
-inbound control channel would work for one node and not the rest, and the difference would be
-invisible until it mattered. **A node has no listening control surface at all.**
-
-**A node holds its own identity and nothing else.** No shared secret, no credential to anything it
-does not own. **Compromise of a node is compromise of that node** — which the current arrangement
-does not have, because every node permanently holds the same database and object-store
-credentials, and there is no mechanism that rotates one and informs everything holding it.
-
-### What that identity is: a keypair the node generates
-
-*Written 2026-08-29. This is the same rule as the sentence above, and it had been treated as an
-open question for weeks because of a word.*
-
-**The node generates a keypair. The private half never leaves the machine. The mesh records the
-public half.** Ed25519, the same as the control plane's signing key, in the other direction:
-the mesh proves itself to a node by signing, and a node proves itself to the mesh by signing.
-
-**This was never open.** [`08-connectivity.md`](../03-DESIGN/01-to-be/08-connectivity.md) already
-says it of the overlay keys, in these words: *each node generates its own keypair, the private key
-never leaves the machine, the public key is published to the mesh* — and adds that this **is**
-ADR 0004's *a node holds its own identity*, applied. What was missing was applying it to the thing
-this record is about.
-
-**The word that caused it:** the lifecycle says a joining node *receives* its own durable identity,
-which reads as the mesh issuing something, and then the question becomes *issuing what*. It does
-not issue anything. The node arrives holding its identity; what it receives is **being known**.
-Enrolment is the moment the mesh writes down a public key it will believe, and the one-time secret
-is what buys the right to have it written down.
-
-**Everything above then holds literally.** Nothing is stored that could be stolen and replayed: the
-mesh's copy is a public key, so a copy of the mesh's database grants nothing. *Compromise of a node
-is compromise of that node* becomes true rather than aspirational, because the only secret on a
-machine is the one that identifies it.
-
-### What "connecting to the mesh" is, concretely
-
-*Written 2026-08-29, because it was asked and this record had never said it.*
-
-**One outbound AMQP connection from the node to the broker, held open.** That is all of it. There
-is no second connection and nothing is ever dialled *at* a node. Being in the mesh, operationally,
-means that connection is up; being disconnected means it is not
-([`09-the-node-lifecycle.md`](../03-DESIGN/01-to-be/09-the-node-lifecycle.md)).
-
-**Two different things ride on it, and conflating them is what made this confusing:**
-
-| | what it answers | who issues it |
-|---|---|---|
-| **an AMQP account** | may this connection be accepted at all | **the mesh, at enrolment** |
-| **the node's keypair** | which node is speaking, on every message | **the node**, above |
-
-**The account is the mesh's to issue**, and per node. The broker has to authenticate somebody
-before a connection exists, and a shared account would let any node consume another's queue —
-which is the shared-credential fault this record exists to remove, reappearing at the transport.
-So enrolment creates that node's account and hands it over, and it is rotatable without touching
-the node's identity.
-
-**The keypair is not made redundant by it.** With only an account, the control plane would know
-which node is speaking *because the broker says so* — and that is the same transitive authority
-this record refuses in the other direction. A compromised broker could then attribute reports to
-whichever node it liked, and the control plane would act on them. Signing is what removes the
-broker from the question in both directions.
-
-**So a node holds two things after enrolment**: a credential the mesh issued for reaching the
-broker, and a key it generated itself that the mesh only ever sees the public half of. Both are
-its own, neither reaches anything else, and *compromise of a node is compromise of that node*
-still holds.
-
-### Its own key, not the machine's SSH host key
-
-Reusing the host key is the obvious economy and it is refused, for reasons that are operational
-rather than fastidious:
-
-- **It is regenerated by ordinary events.** A reinstall, an image cloned, `ssh-keygen -A` on a
- rebuild — each silently un-enrols the node, and the failure appears as an authentication problem
- with no cause anybody changed.
-- **It is managed by something else.** Its lifecycle belongs to the machine's SSH daemon, and an
- identity the mesh depends on should not rotate on a schedule the mesh does not know about.
-- **Not every node has one.** A partial host has no SSH daemon
- ([ADR 0005](0005-the-node-host.md)), and an identity scheme that excludes a supported kind of
- node is not one.
-
-**The mesh should still know the host key** — it knows every node, so it can distribute host keys
-the same way it distributes authorised keys
-([ADR 0006](0006-the-substrate-and-the-control-plane.md)), and node-to-node SSH stops depending on
-trust-on-first-use. That is the good half of the idea, kept.
-
-**Authority is mutual.** The node proves it may join, and the control plane proves it is the
-mesh. One-way is not enough: the host applies whatever the link delivers, so a node that cannot
-tell the mesh from something impersonating it will apply that something's declarations.
-
-**What may be pushed is bounded by form, not by trust.** Declarations of known shape, never a
-command to run. Stated honestly, **this bounds form and not impact**: a compromised control plane
-can declare harmful state and the host will apply it faithfully, because that is what it is for.
-What the property buys is that the blast radius is *describable* — exactly what the declaration
-language can express, which can be reviewed. An arbitrary command channel has no such bound.
-
-## The enrolment token carries the mesh
-
-Mutual authority needs the node to verify something before it trusts anything, and that is a
-circle: verifying the mesh needs the mesh's certificate authority, and obtaining one means
-trusting whoever hands it over. There is a second circle beside it — a node must reach the mesh
-before the mesh has configured it, so it can resolve no mesh name.
-
-**Both are the same shape: a node needs a fact about the mesh before it has any trustworthy way
-to obtain one.** So that fact arrives by a path other than the mesh.
-
-**The token carries five things**, and it is the only thing a joining node needs:
-
-| | |
-|---|---|
-| **who it is** | the name the mesh calls this machine |
-| **where** | the broker's **address**, not a name — there is no resolution yet, and this is why none is needed |
-| **what it is connecting to** | the fingerprint of the broker's certificate |
-| **who it will believe** | the control plane's signing identity |
-| **the right to join** | a one-time secret, useless once used and useless after it expires |
-
-*The first row was added 2026-08-30, from raising a mesh end to end for the first time.* It reads
-like an oversight and is not: **the node cannot work its own name out.** The name is the mesh's,
-chosen when the record was created, and the broker account the node authenticates as is named
-after it — so it must be known *before* the mesh can tell the node anything. It is not a secret,
-and whoever issues the token already has it.
-
-Without it, enrolment fails at the broker with an empty username and a message about credentials,
-which points at everything except the cause. **A missing fact that surfaces as an authentication
-error is worse than one that surfaces as a missing fact.**
-
-**Carried out of band**, by the person adopting the machine. That is what breaks both circles:
-its authenticity comes from the channel it travelled, not from anything the node can check
-afterwards. **Trust on first use, with the first use moved out of band** — the difference between
-a pin and a guess.
-
-**The endpoint and the authority are two identities.** A node connects to the broker and takes
-instruction from the control plane behind it. Pinning only the broker would make the control
-plane's authority *transitive*, and a compromised broker could then forge declarations — which,
-since the host applies whatever the link delivers, is the whole machine. So the transport is
-verified once at connect, and **each declaration is verified by its signature, every time**.
-
-**What this settles:** the mesh's certificate authority is not a bootstrap concern — it certifies
-internal names once a node is a member. Nothing needs name resolution before the link. And
-nothing is placed on disk beforehand except the token, which is the first moment *a node holds
-only its own identity* becomes true rather than aspirational.
-
-## Consequences
-
-- **Declarations must be signed**, and the host must tell *this is not from the mesh I joined*
- apart from *this is malformed*. Rotating the signing identity is a fleet-wide operation with an
- overlapping rollover, and that is the cost of not trusting the broker.
-- **The token becomes security-critical**, because it carries the pin. Tampering substitutes the
- mesh — which is strictly better than the alternative, where there is nothing to tamper with and
- the node trusts the first answer unconditionally.
-- **A rejoining node is ordinary.** There is no long-lived secret to recover, so a node that lost
- its identity gets a new token.
diff --git a/02-DECISIONS/0005-the-node-host.md b/02-DECISIONS/0005-the-node-host.md
deleted file mode 100644
index 7afc763b..00000000
--- a/02-DECISIONS/0005-the-node-host.md
+++ /dev/null
@@ -1,155 +0,0 @@
----
-topic: the tiers
-status: accepted
-date: 2026-08-28
-deciders: jochen
-reconstructed: false
----
-
-# 5. The node host
-
-*Consolidated 2026-08-28 from eight records. Tier 0 is one component and was decided over a
-week; the reasoning is kept, the fragmentation is not.*
-
-## It applies; it does not decide
-
-**The host makes a machine match what it was told, and never works out what that should be.**
-
-This is the line the whole tier rests on, and it is not about privilege — it is about what a
-single machine can *know*. Deciding needs knowledge the machine does not have: which nodes should
-run a store, which peers belong in an overlay, whether a node has been unreachable for a week.
-Anything needing a second node is the control plane's.
-
-The practical form: **the host never queries the mesh's database and holds no credential to it.**
-Two modules in the current mesh do, and they are the reason every node permanently carries a
-database credential.
-
-## It depends on nothing that must be installed first
-
-**A statically linked binary. Copy it onto a machine and run it — that is the whole
-installation.** Written in Go, because the job is system-level and because a runtime that must be
-installed first would make the host depend on the thing it exists to install.
-
-**What it needs from the machine is not a dependency in this sense.** An init is not installed;
-it is what the machine already is. A package manager is the distribution. Those are what a
-machine *is*, not what must be put on it before the host works.
-
-## It is built per operating system
-
-**`systemd` and `pacman` are the Arch host's implementation, not abstractions the mesh grows.**
-They are not independent choices: a machine has pacman *because* it is Arch, and the package
-manager, service manager and packaging format arrive together as one decision somebody made at
-install time.
-
-```
-mesh-host-arch pacman · systemctl · a container runtime
-mesh-host-alpine apk · rc-service
-mesh-host-android neither — a partial host
-```
-
-**Abstracting them was rejected on correctness, not effort.** The service applier reads systemd's
-`LoadState` to tell *not installed* apart from *stopped* — which is what stops it reporting
-absence as success — and OpenRC has no equivalent. An interface spanning both must drop it, and
-the lowest common denominator is exactly where that fault lives.
-
-**Almost all of it is shared.** The declaration vocabulary, the store, the apply loop, the
-read-back discipline, the refusal model and the link are portable. Two appliers differ.
-
-**A host that cannot implement a shape refuses it.** Android has no package manager it may drive
-and no init it may register with, so it implements `file`, `directory` and `action` and refuses
-the rest — the same refusal an unknown type gets, with a different reason. Those three are the
-portable floor, and they are what makes a partial host a real thing rather than a broken one.
-
-## It is a root service, and it never manages its own unit
-
-**Root**, because no useful part of the job is unprivileged: it writes under `/etc`, installs
-packages, manages units and runs containers.
-
-**It cannot run in a container**, and the reason is decisive rather than stylistic: installing the
-container runtime is a step of the bootstrap, so a host inside a container would need the thing
-it exists to install. Everything above tier 0 is a container; the host is not. That split is the
-tier boundary made concrete.
-
-**The installation owns the host; the host owns everything else.** It manages `service` resources
-and its own unit is one — the temptation is obvious and it ends with a host stopping itself half
-way through an apply, leaving a machine with nothing running to fix it.
-
-## An init is asked for one thing
-
-**Start this at boot.** That is all, and every init can express it — systemd, OpenRC, runit, s6.
-
-**Everything else is a launcher the host ships**, which supervises it: restart it when it exits,
-count consecutive failures, roll back after too many, halt after that. Policy in a unit file can
-only be read and hoped for; a script with a counter can be tested, and this is the one piece that
-must work on a machine where the host does not.
-
-**The launcher does not exec the host, it supervises it** — so restarting is ours rather than the
-init's. The cost is signals: a supervisor that exits while its child runs leaves the host to be
-*killed* rather than to *stop*, and an apply interrupted that way is the half-configured machine
-this design is about. So it traps the shutdown signal, passes it down, and waits.
-
-**A clean exit is the upgrade path**, and it is the easiest thing to get wrong — twice now. The
-host stands aside for a new binary by exiting zero, so anything supervising must restart on a
-zero exit and must not count it as a failure.
-
-**Recovery is local, and detection is the mesh's.** Nothing dials a node and a host that cannot
-start cannot report, so the node must recover itself. But a local supervisor sees one process
-failing and cannot tell a broken machine from a broken release — only something watching every
-node can, which is why a host rollout is staged and stops when nodes go quiet.
-
-## A host may be episodic
-
-**Resident or episodic, and both are hosts.** A phone has no init to register with and nothing
-worth supervising, because a supervisor would be killed alongside what it supervises. So it runs
-when the platform allows and is killed when the platform wants the memory — **and that is
-disconnection**, which is already an ordinary situation.
-
-It needs no keep-alive and no new mechanism: the store is already authoritative while
-disconnected, reconcile already happens on start, and *last heard from* is already reported
-rather than alarmed on. An episodic host cannot be the first node, because every bootstrap step
-is a shape it refuses.
-
-## What a declaration is
-
-**An ordered list of resources the host owns.** JSON, because Go's standard library carries a
-JSON parser and no YAML, and the one binary whose argument is that it needs nothing must not
-gain a parser to buy authoring comfort in a machine-written document.
-
-**Ordered, because ordering is a decision.** The host does not sort and does not resolve
-dependencies — that would be deciding, and deciding the thing most likely to differ between what
-the control plane intended and what the machine does.
-
-**Every resource has a stable identity** — a name the control plane keeps across declarations, not
-a position and not a hash of content. It is what lets the store say *this is the same resource I
-applied last time*, which is what makes removal possible at all.
-
-**Unknown is refused, whole.** A field the host does not know is something the control plane
-believes it asked for. A declaration naming one is rejected entirely, naming every problem at
-once — a host that applied the parts it understood would leave a machine that looks configured
-and is not.
-
-**Six shapes:** `file`, `directory`, `service`, `package`, `container`, `action`. Every addition
-widens what a compromised control plane can express, so the list is a security artefact and grows
-deliberately.
-
-### The bundle may carry actions; the link may not
-
-An `action` runs a command, and the host never learns what it means. It is needed because the
-bootstrap creates a database before there is any mesh to ask for one, and the host must not learn
-what a database is.
-
-**Permitted from the bundle, refused from the link**, and the asymmetry is the whole point: a
-bundle arrives *with* the binary, so anyone able to put a hostile action there could have put it
-in the host itself — refusing it buys nothing and costs the bootstrap. The link is a separate
-party, reachable separately, and an action there is an unbounded blast radius.
-
-**An action must carry its own verification**, which is also its idempotency check. The host does
-not know what a database is, so *is it already there* is a question only the declaration can ask.
-
-## Consequences
-
-- **The migration is smaller than it looks.** A joining node never needs mesh-wide state — it
- needs an identity, an address and one peer, and the rest arrives as declarations.
-- **What is applied is recorded after it works, never before.** A failed apply leaves the machine
- in whatever state it reached, and nothing must claim otherwise.
-- **A second operating system is additive**: two appliers and a four-line init file.
diff --git a/02-DECISIONS/0006-the-substrate-and-the-control-plane.md b/02-DECISIONS/0006-the-substrate-and-the-control-plane.md
deleted file mode 100644
index eacc6791..00000000
--- a/02-DECISIONS/0006-the-substrate-and-the-control-plane.md
+++ /dev/null
@@ -1,284 +0,0 @@
----
-topic: the tiers
-status: accepted
-date: 2026-08-28
-deciders: jochen
-reconstructed: false
----
-
-# 6. The substrate and the control plane
-
-> **Superseded in part — 2026-10-07, by [ADR 0244](0244-the-mesh-is-described-in-domains-and-one-word-names-one-thing.md).**
-> The seven contexts below no longer describe the mesh: it is described in ten domains, and "domain" replaces
-> "context" in this sense; *observability* becomes Health and repair. This record keeps its word, and the
-> glossary says how to read it. Everything else decided here stands.
-
-*Consolidated 2026-08-28 from six records. Extended 2026-08-29, by building it: the language, and
-what must be running before the control plane starts — which this record had left not established
-and could not have settled the way it was asking.*
-
-## The control plane is what needs to know about more than one node
-
-That is the whole test, and it follows from the host applying rather than deciding: **deciding
-needs knowledge a single machine does not have.**
-
-| question | whose |
-|---|---|
-| write this file, with this content, with this mode | the **host** |
-| which nodes should run the store | the **control plane** |
-| is this unit running | the **host** |
-| which peers belong in this node's overlay | the **control plane** |
-| has this node been unreachable for a week | the **control plane** — nobody else is watching |
-
-**Anything a single machine could answer alone is not the control plane's.**
-
-### Seven contexts and one interface
-
-**inventory, config, connectivity, provisioning, delivery, observability, identity** — plus
-`api`, the one interface every surface speaks to. Each earns its place by the test above rather
-than by being ours.
-
-**`work`, `knowledge` and `stream` are mesh-hosted applications, not control plane.** A task does
-not need to know a node exists. *Being ours does not make something infrastructure.*
-
-**`identity` owns SSH access.** *Written 2026-08-29, on noticing it was assumed everywhere and
-stated nowhere.* SSH appears three times across this design and every time as something that
-*uses* the overlay — "the way back in", "every node reaches every other: SSH, services, ordinary
-traffic" — while nothing said who hands out the keys. Nobody else could: the mesh is the only
-thing that knows which humans and agents exist and which nodes they may reach, which is
-`identity`'s definition. The node end already works, since an `authorized_keys` file is a file.
-
-It is three questions wearing one name, and only two of them are the mesh's:
-
-| | |
-|---|---|
-| **humans** | their key, on the nodes they are allowed on |
-| **agents** | the same, with a lifetime — and revocation that has to actually work |
-| **an agent reaching another node** | **this is the point of the mesh, not an exception to it** — see below |
-
-**There is no such thing as node-to-node SSH here, and that is a clarification rather than a
-restriction.** The actor is always an **agent**; a node is only where it happens to be running —
-[ADR 0001](0001-mesh-brokers-nodes-host-agents-think.md): *a node is a place where an agent can
-run, that is the entire relationship.* An agent hired onto one node reaching another to do work is
-the capability the whole arrangement exists to provide.
-
-**The credential is the agent's, never the node's.** It lives in the agent's own credential
-directory ([ADR 0001](0001-mesh-brokers-nodes-host-agents-think.md)), so a node's
-`authorized_keys` lists **agents** and never nodes. Three things follow, and they are why this
-shape is better rather than merely allowed:
-
-- [ADR 0004](0004-a-node-and-how-it-joins.md)'s *a node holds its own identity and nothing else*
- stays true — no node holds a key that reaches another node;
-- a compromised node costs the credentials of the agents that were on it, not a way into
- everything;
-- **who may reach what is a mesh-wide fact**, which is exactly why it is `identity`'s and not
- something arranged locally.
-
-**And it does not conflict with the host having no inbound control surface.** That rule is about
-how a node's *declared state* changes: over the broker, never by being dialled. An agent with a
-shell is not the mesh reconfiguring a machine — it is what a person with a terminal has always
-been, and this design already depends on it working
-([ADR 0007](0007-connectivity.md): the overlay is *the way back in*). What such a session can leave
-behind is drift, and drift is what reconciliation is for.
-
-**Where the record lives is deliberately open.** Contexts integrate through it, which makes it
-load-bearing, and putting it in the substrate risks recreating the circularity the tiers just
-removed. Listing it as an eighth context would settle by naming what has not been settled by
-arguing.
-
-### One node runs it, and nothing takes over
-
-**Declared, never elected.** No promotion, no quorum, no fencing, no split brain — none of it
-built, so none of it can be subtly wrong.
-
-#### The option that would make it a real mesh, and why not
-
-*Written 2026-08-29. It had been rejected by never being written down, which is the weakest way
-to reject anything.*
-
-A genuine peer-to-peer mesh means **no node is special**, and that has a concrete price:
-
-- every node holds the **whole inventory**, so there is a replication process between them;
-- replication needs a writer, so one node is elected **master**, and something promotes a new one
- when it drops — Redis Sentinel and its whole family of problems;
-- and it still would not deliver what the name promises, because **application databases are not
- replicated.** A workload's store lives where the workload lives.
-
-That last point is the one that settles it. To make the mesh genuinely peer-to-peer we would have
-to become **a replicated database system for everything running on it** — not for our own
-inventory, for every consumer's data too. That is a product, and a much larger one than the thing
-it would be supporting.
-
-**So there are three central roles, not one**, and it is worth seeing them separately because
-only the third costs operation:
-
-| | its loss costs |
-|---|---|
-| **the control plane** | nothing can be *changed*. Nothing stops running |
-| **the broker** | nothing can be told anything, or report anything |
-| **the hub** | nodes in different places **cannot reach each other** ([ADR 0007](0007-connectivity.md)) |
-
-**Whether these are one node is not decided here.** All three must be dialable by every node, which
-pushes toward one; nothing says they must be.
-
-**That is sound rather than merely cheap**, because the design already tolerates its absence by
-construction: a node reconciles from its own store and never needed to ask anybody to hold the
-state it was last given. **The control plane being down is not a new failure mode — it is every
-node in the ordinary disconnected situation at once.** What is lost is *change*, not *operation*.
-
-The honest half: this node is a single point of failure, recovery is **restore rather than
-failover** — which makes backup the availability mechanism rather than hygiene — and
-**certificate renewal is the clock.** An outage outlasting a renewal window expires every public
-name, which turns an inconvenience into an outage on a timer. Nothing measures that today.
-
-## The authority is the control plane, not a database
-
-**There is no single mesh database.** Each context owns its store exclusively, and *the mesh
-database* names a thing that will not exist.
-
-**No node reads any of them** — not for writes, not for reads. A node is *told* what to own, over
-the link, in a bounded vocabulary; it **states** what it applied, and the owning context writes.
-The difference is the security boundary: something that can write cannot be prevented from
-writing anything.
-
-**A node runs from its own store always, not as a fallback.** The current arrangement's nastiest
-property is that *a node running from cache looks identical to a node running from the database*,
-with no age on the cache and nothing reporting divergence. Under this there is no second mode to
-be mistaken for the first.
-
-**What survives from the original decision:** the repository defines what exists, the mesh defines
-what runs where, and no node-to-module mapping is ever committed. That is what makes the
-repositories node-agnostic and why anything about the mesh can be published at all.
-
-**The error underneath was a category error**: *source of truth* named a storage location when it
-meant an **authority**. Once the store is the answer, *which database* becomes the question, and
-shared schemas follow.
-
-## The substrate is what the control plane consumes and cannot grant itself
-
-Every module needing a database asks provisioning for one. The control plane needs a database too
-and cannot ask itself, because it is not running yet. **That circularity is the definition**, and
-anything on the wrong side of it is raised from the bundle the host carries.
-
-| role | product | |
-|---|---|---|
-| relational store | **PostgreSQL** | its own state lives there |
-| message bus | **LavinMQ** | it cannot grant itself a virtual host — and **precedes it**, below |
-| object store | **MinIO** | it cannot grant itself a bucket |
-| image registry | **an OCI registry** | it cannot grant itself a repository |
-| identity provider | — | **conditional**: substrate only if the control plane delegates authentication, which is undecided |
-
-**The role and the product are both written.** The role is what the argument turns on; the product
-is what gets installed and pinned, and a design that names only the role does not record that the
-choice was made. **The dependency is on the protocol** — AMQP, S3, OCI — which is what keeps
-naming them safe. The store is the exception: the provisioning model uses databases, roles and
-schemas as PostgreSQL means them.
-
-**A container runtime is detected, not chosen** — docker or podman, because a machine that
-already has one keeps it. Only the version probe differs between them; the behavioural difference
-(podman has no daemon, so containers do not return after a reboot unless a unit is enabled)
-belongs in the declaration rather than the host.
-
-**Being substrate and being in the bundle are different questions.** PostgreSQL and LavinMQ must
-precede the control plane; the object store and the registry are substrate by role and ordinary by
-delivery, provisioned once there is a control plane to do it.
-
-### Why the broker precedes it too
-
-Written after the fact, because this record first left it *not established* and framed it as
-turning on whether the control plane's own contexts talk to each other over the bus.
-
-**They do not** — they are one process and dispatch internally. Under that framing the broker is
-provisioned like anything else and the bundle stays at one image.
-
-**The framing cannot answer the question.** What decides it is not how the contexts reach each
-other. It is how the control plane reaches a *node* — and that is settled above: never except over
-the link, and the link is AMQP ([ADR 0002](0002-nodes-communicate-over-a-broker.md),
-[ADR 0004](0004-a-node-and-how-it-joins.md)). So:
-
-```
-the bundle raises the control plane
-the control plane provisions the broker ← by telling a host to run it
-telling a host happens over the link
-the link is the broker
-```
-
-**And it is not avoided by the first node being local.** `enrol` dials the broker at the address in
-its token, which is the first node's own third step. A machine that raised the mesh still joins it
-the ordinary way, and that was deliberate — its specialness lasts two commands. Making it join by
-some other route would buy a smaller bundle by giving up the property the design was built to have.
-
-The broker precedes the control plane for the same reason PostgreSQL does: **the control plane
-cannot grant itself the thing it would need in order to grant it.**
-
-**What it costs.** Two images rather than one, against the wish above to keep the bundle at roughly
-one so a person can read it — two is still readable, four would not be. And three things that are
-not images, each an action the bundle declares and the host runs, the way the database already is:
-a virtual host, a credential on it, and **a certificate**. That last is the awkward one: a token
-pins the fingerprint a host must expect *before it sends anything*, so the broker needs a
-certificate at a moment when there is no mesh to issue one and no public name to obtain one for.
-Self-signed and pinned is the shape that fits; how it is later replaced by the certificates in
-[ADR 0007](0007-connectivity.md) is not decided here.
-
-## The control plane is written in Go
-
-The same language as the host, so tiers 0 and 2 are one language and not two.
-
-The reason that decides it is not familiarity. **Its image is pinned by digest in the bundle**,
-which means it is fetched and run on a machine where no mesh exists yet — nothing to check it
-against, nothing watching, and a person expected to have read the bundle and believed it. A
-statically linked binary makes that image the program and nothing else: no interpreter, no package
-tree, no transitive dependency that arrived because something needed a date library. Everything
-under that line is something somebody would have to audit, on the one image the whole mesh is
-raised from.
-
-A second reason, smaller and still real: the control plane runs a reconcile loop of its own
-([ADR 0010](0010-delivery.md) — artifacts against source, as the host reconciles machine state
-against declarations). Two loops of the same shape are cheaper to hold in one head when they are
-also the same language.
-
-**The option rejected** is TypeScript, matching the lab and the surfaces that will speak to this.
-The argument for it is that tier 3 is web and CLI, so a TypeScript control plane would share types
-with its callers rather than generating a contract. True, and it does not reach far enough:
-`mesh-sdk` is *contracts shared across tiers* and **tier 0 is Go**, so the contracts cross a
-language boundary whatever tier 2 is written in. The choice is between generating them for one
-consumer or for two.
-
-**What it costs, plainly:** the control plane can import nothing that exists today, and a person
-moving between tier 2 and tier 3 changes language. Neither is recovered later — the language is the
-most expensive thing in this record to reverse.
-
-## The installer fetches what it pins
-
-`substrate.lock` carries **references, not payload** — an image name and a **digest**, fetched at
-apply time. A tag moves; a digest does not, and reproducibility comes from pinning the identity of
-a thing rather than carrying its bytes.
-
-The assumption that a machine might have no network came from the lab and was wrong: a machine
-being adopted has one, and the sealed case is the lab.
-
-**The lab places images by raising a registry inside the scenario**, which is what a real node
-pulls from anyway — so it tests the real path rather than a stand-in for it. The digests that
-registry serves are its own, and that satisfies this rule: what is required is a reference that
-is **exact and cannot move**, and one it assigned is both. Assuming an upstream digest had to be
-preserved is what made this look impossible for a while
-([04-ISSUES/009](../04-ISSUES/009-a-digest-pinned-image-cannot-be-placed-in-the-lab/00-report.md)).
-
-**Its contents are per operating system** even though its mechanism is not — package names, unit
-names and service names all differ, so an Arch host embeds an Arch bundle.
-
-## Consequences
-
-- **The bundle stays small and reviewable.** A list of pinned references is something a person can
- read; a bundle containing images is not.
-- **An apply can fail because something is unreachable**, which a self-contained artifact could
- not. That must fail *legibly*, naming what could not be fetched and from where.
-- **Cross-context reporting is harder, and that is the point.** Anything wanting to see across
- contexts consumes their events or calls their interfaces.
-- **A queue with no limit grows until the broker's disk is full**, and the broker is what every
- node depends on. The bound is per queue and is not decided.
-- **The bundle carries two images and four actions**, and the substrate bootstrap grows a step.
-- **Nothing in the first node's path is special-cased.** Enrolment is walked on node one.
-- **The broker's certificate at bootstrap has no answer yet**, and is named as unfinished rather
- than assumed. It is the first thing that will be wanted when the link is built.
-- **The language cannot be revisited cheaply.** It is the one line here close to irreversible.
diff --git a/02-DECISIONS/0007-connectivity.md b/02-DECISIONS/0007-connectivity.md
deleted file mode 100644
index f70d7842..00000000
--- a/02-DECISIONS/0007-connectivity.md
+++ /dev/null
@@ -1,191 +0,0 @@
----
-topic: the tiers
-status: accepted
-date: 2026-08-28
-deciders: jochen
-reconstructed: false
----
-
-# 7. Connectivity
-
-*Consolidated 2026-08-28 from three records. Overlay, resolution, exposure, filtering and
-certificates are one design.*
-
-## Why it is control-plane work
-
-Apply the test — *everything that needs to know about more than one node* — and not one of the
-five can be answered by a machine on its own:
-
-| | needs to know |
-|---|---|
-| **overlay** — who peers with whom | every node, and which can be dialled |
-| **resolution** — which name is which node | every node |
-| **exposure** — which public name reaches which container | which node is publicly reachable |
-| **filtering** — which port is open, to whom | what is assigned here, and the overlay's shape |
-| **certificates** — who may present which name | which name belongs to which node |
-
-That is exactly what the current arrangement gets wrong, by computing all five on the node from a
-direct database connection. Two modules do this, and they are the only two left holding a
-credential to the control plane's database.
-
-**The shape of the fix, once for all five:** the connectivity context computes the configuration;
-it arrives over the link as `file` resources; the service reads files and knows nothing about the
-mesh. **This costs no new host vocabulary.**
-
-## A route is a grant
-
-**Ingress is not substrate.** The control plane does not need a route to start — it listens
-locally — and no node needs one to reach it, because the node dials out and has no listening
-control surface. It grants itself a route afterwards, the way it grants itself a bucket.
-
-The strongest objection deserves stating: the `api` is the one interface every surface speaks to,
-so eventually it *does* want a public name. But **wanting one later is not needing one to
-start**, and that distinction is the entire substrate test.
-
-**A module that must be reachable declares it needs a route; the proxy provides one.** Ordinary
-instantiation, with the direction mirrored — the consumer supplies a target and receives a name.
-
-**Exposure is three facts at two scopes**, which is why it cannot live on the node:
-
-| the fact | scope |
-|---|---|
-| the public name resolves to an address | **mesh** — which node is publicly reachable |
-| a certificate valid for that name exists | **mesh** — issued once, used on one node |
-| the proxy maps that name to that container | **node** |
-
-**A node without a public address is proxied by one that has**, across the overlay. Most nodes sit
-behind a connection with no forwarded port, so exposure cannot assume the workload's node is
-reachable.
-
-## Reachability is declared, not inferred
-
-The overlay's peer graph is computed from whether a node can be dialled, and that was inferred
-from a regular expression over the address. **The address is evidence of reachability; it is not
-the fact**, and the gap has already cost:
-
-| address | the regex says | actually |
-|---|---|---|
-| `100.64.0.0/10` — carrier-grade NAT | **public** | **not reachable.** An endpoint is written to an address nothing can reach |
-| any IPv6 address | public | the test is v4 shapes only |
-| a routable address behind a closed firewall | public | not reachable |
-| a documentation range standing in for a public segment | private | reachable — this is the lab bug |
-
-**A test environment having to choose its addresses to satisfy a regex is the regex telling us it
-is not a fact.**
-
-So: **an endpoint, or none** — declared. And **the hub is declared, never derived from an address
-prefix**, because an election decided by the first four characters of an address fails silently,
-cannot be queried, and makes a renumbering an outage.
-
-**The address remains evidence and stops being the fact.** Where an observed endpoint disagrees
-with a declared one, the disagreement is a **reportable condition**, not a silent correction.
-
-**What does not change** is the lesson underneath: role does not imply reachability — a
-home-hosted node is a server that cannot be dialled. This keeps that and stops encoding it as a
-pattern match.
-
-### Some nodes must be reachable, and this had not been said
-
-*Written 2026-08-29, on being asked and finding no answer.*
-
-Everything above treats reachability as a **fact to record** — which node can be dialled, so that
-exposure and certificate issuance can be placed. It never said the converse, and the converse is a
-hard requirement:
-
-| role | dialled by | so it needs |
-|---|---|---|
-| **the node running the broker** | every node, outbound ([ADR 0002](0002-nodes-communicate-over-a-broker.md)) | to be reachable from wherever nodes are, at a **stable address** |
-| **the hub** | every node not co-located with its peer | the same |
-
-**Across the internet, "reachable from wherever nodes are" means publicly reachable.** For a mesh
-confined to one network it does not — the requirement is about the nodes that exist, not about the
-public internet.
-
-**Stable is the sharper half.** A token carries the broker's *address, not a name*, because there
-is no resolution before joining ([ADR 0004](0004-a-node-and-how-it-joins.md)). A broker node whose
-address moves invalidates every token issued for it, and a node that was disconnected across the
-change cannot get back.
-
-**A mesh whose nodes are all behind NAT cannot be raised.** That is a real precondition and it
-belongs with the others rather than being discovered.
-
-### The link stays on the underlay, and that is a repair channel
-
-The obvious objection is that all traffic should run over the overlay. Nearly all of it does — SSH,
-services, node to node — and the exception is each node's own outbound link to the broker.
-
-**At join time it is forced**: a node has no overlay yet, so it cannot use one to ask for one.
-**Afterwards it is a choice**, and the reason is that the link is how a broken node is fixed. A
-repair channel carried over the thing being repaired is not a repair channel: a node whose only
-path home was the overlay is gone the moment an overlay declaration is wrong.
-
-**What it does not cost is confidentiality.** The link is already authenticated and encrypted
-against a pinned fingerprint ([ADR 0004](0004-a-node-and-how-it-joins.md)), so moving it onto the
-overlay would not protect traffic that is unprotected today.
-
-## A filter rule names its source
-
-`scope: public` is declared in five manifests, is part of no rule type, and is **referenced by no
-code**. So five manifests appear to restrict a port and restrict nothing — on the modules most
-worth restricting.
-
-**A rule names its source. `from:` is the only way to scope one, and a rule without one is open**
-— which it must say plainly rather than appear to deny.
-
-**`scope:` is removed rather than implemented**, because giving it meaning would leave two ways to
-express one thing. And the general fix is that **an unknown key is refused**: the host's
-declaration parser already works this way, and manifests are the layer where that discipline is
-missing. `scope:` survived because nothing rejected it, and it spread by copying to five
-manifests.
-
-## Order, and what it costs
-
-**The link runs on the underlay and never on the overlay.** The overlay is configured by the mesh,
-so a link requiring it could never be established on a new node.
-
-**The first declaration is the overlay and nothing else** — because a node's address and peers are
-*assigned* so it cannot come earlier, and because it is the way back in. A node reachable over the
-overlay can be fixed by hand if a later declaration breaks the machine; **a large first
-declaration risks a node that is broken and unreachable at once.**
-
-**Reachable is not the same as having a control surface.** Every node reaches every other over the
-overlay — SSH, services, ordinary traffic — and every node consumes from the broker. What is
-forbidden is a listening thing that accepts instructions and changes the machine.
-
-## Consequences
-
-- **The last two direct database connections leave the nodes**, and with them the database
- credential every node carries.
-- **The `/etc/hosts` floor goes**, along with the bootstrap circularity it patched.
-- **Two certificate authorities stay separate on purpose**: a public one for public names, the
- mesh's own for internal ones. A single-CA lab would hide any bug living in the split.
-- **What happens when the hub is down**: nothing takes over. Non-co-located paths stop; co-located
- peers and every assigned workload keep running.
-
-## Open — the link over the overlay, with a fallback
-
-*Raised 2026-08-29 and deliberately left open, because the honest gain is smaller than it looks
-and it is a decision rather than a derivation.*
-
-The proposal: a node prefers the overlay for its link and drops to the underlay when the overlay
-is not working — so ordinary operation is private and the underlay stays as the way back.
-
-**Two things it would have to get right:**
-
-- **The trigger cannot be "is the overlay up".** A WireGuard interface has no link state; once
- configured it is up whether or not the far end exists. So there is no flag to read, and failing
- over means *try, fail, time out, retry elsewhere*.
-- **Running on the fallback has to be visible.** A node that quietly drops to the underlay is a
- node whose overlay is broken with nothing to say so, and it will stay broken because everything
- still works. That is this repository's recurring fault — a failure that reads as success — and a
- fallback is the easiest place in the design to reintroduce it.
-
-**What stops it being an obvious win:** if the underlay path must stay available for the fallback,
-the broker stays exposed on it. So the exposure is unchanged and the traffic was already encrypted
-— the gain is which network carries bytes, not what an attacker can reach.
-
-**The version that would buy something is overlay-only**, with the broker firewalled to the overlay
-in steady state, accepting that a node whose overlay breaks needs hands-on recovery. That is a real
-trade: it exchanges the automatic way back in for a closed port.
-
-Not decided either way here.
diff --git a/02-DECISIONS/0008-a-context-owns-its-store.md b/02-DECISIONS/0008-a-context-owns-its-store.md
deleted file mode 100644
index ab3c9968..00000000
--- a/02-DECISIONS/0008-a-context-owns-its-store.md
+++ /dev/null
@@ -1,114 +0,0 @@
----
-topic: the tiers
-status: accepted
-date: 2026-08-26
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0009-modules-and-the-graph.md
----
-
-# 8. A context owns its store, exclusively
-
-## Context
-
-[ADR 0009](0009-modules-and-the-graph.md) settles what a module
-declares. This settles what a grant may be, and it is the half that **removes** things.
-
-`how-we-build` §4 already says *contexts integrate through the record, never through a shared
-schema*, and states the cost: several domains share one forty-five-table schema, which is why
-work belonging to one context keeps having to be implemented in another.
-
-That was written as a principle. Counted, it is thirteen foreign tables belonging to three
-separate contexts, living in the mesh's own registry database.
-
-## Considered options
-
-1. **A schema per consumer inside a shared database.** Namespaced, revocable by dropping the
- schema, with a cross-context join possible but deliberate. Rejected: it keeps the letter of
- §4 and leaves the temptation in place, and a boundary that is merely inconvenient to cross
- gets crossed.
-2. **Read-only roles on another context's store.** Rejected for the same reason and one worse:
- reading another context's tables couples you to its layout exactly as firmly as writing them,
- and the coupling is invisible until the owner changes a column.
-3. **Exclusive ownership.** Chosen.
-
-## Decision
-
-> **A context is granted only what it exclusively owns.**
-
-No shared writes. No read-only role on another context's store. If you need what another context
-holds, you ask it or you subscribe to it.
-
-**The unit is the context, not the process.** Everything inside a context — its service, its
-surface, its tools — reads its own store freely. A board showing the mesh's own nodes and
-modules is the mesh showing its own data, not a boundary crossing. What is forbidden is a
-*different* context reading it.
-
-### Asking or subscribing is derived, not chosen
-
-[ADR 0004](0004-a-node-and-how-it-joins.md) makes disconnection an ordinary situation. So:
-
-- **Anything that must keep working while disconnected cannot ask** — there is nobody to ask. It
- keeps a local copy, which means subscribing.
-- **Anything where a stale answer is worse than none cannot subscribe.** A display may lag; a
- decision about whether a grant is still valid may not.
-
-Neither is a query against another store, whatever transport it travels over.
-
-### What that is, concretely
-
-*Written 2026-08-29, on building the first one — the rule above was clear and what to type was not.*
-
-**One PostgreSQL database per context, named for the context.** A separate database rather than a
-separate schema is the whole point: a cross-schema join is a qualified name away, and a
-cross-database join needs a foreign data wrapper somebody has to install and explain.
-
-**And one credential per context, held only by it.** There is no mesh-wide connection setting and
-no way to ask for one, so reaching another context's store is not a matter of restraint — a process
-has no address for it and nothing to present. That is also how this rule is *checked*: what a
-context can reach is the list of variables the declaration running it grants, and it is read there
-rather than audited in code.
-
-**Contexts that do not exist yet do not get a database.** The bootstrap creates the ones there are.
-
-**How a context added later gets its database is open**, and it is a real question: by then there is
-a control plane, but a control plane holding a credential that can create databases is holding
-rather more than the thing it exclusively owns.
-
-## What this removes
-
-The first clear list of what the design deletes rather than adds:
-
-- **Grant kinds.** There is one: an exclusive resource. No schema grants, no read roles, no
- rules about who may see what inside a shared thing.
-- **The question of who owns which table**, and the guessing at revocation time. Removing a
- consumer drops what it was granted, whole.
-- **Cross-context migration ordering.** Two contexts migrating one database must be ordered
- against each other. Exclusive ownership means a context's migrations are ordered only against
- itself.
-- **A class of permission modelling** a shared store would otherwise need.
-
-## Consequences
-
-- **Cross-context reporting is harder, and that is the point.** Anything wanting to see across
- contexts consumes their events or calls their interfaces. That is §4's argument, and the cost
- it names is the one already paid.
-- **A single surface over several contexts still works** — that is what a surface is. It reads
- interfaces, not stores. This holds while the contexts sit behind **one** interface; splitting
- a context into its own deployable costs that, and the composition would have nowhere to live
- that tier 3 permits. **A real constraint on how far the control plane may be split.**
-- **Three contexts must move out of the registry database**, taking thirteen tables with them.
- Their dependency on the registry then shrinks to almost nothing — one of them needs a single
- table.
-- **The node appliers were already handled.** [ADR 0005](0005-the-node-host.md)
- stopped the host querying the mesh database for tier reasons unrelated to this, and it removes
- most of the remaining direct readers as a side effect.
-- **What a consumer does about events missed while disconnected is not decided** — replay from a
- point, ask once and resume, or rebuild. The question every projection has.
-
-## References
-
-- [`how-we-build.md`](../00-META/how-we-build.md) §4 — the rule this makes enforceable.
-- [Research 011](../01-RESEARCH/011-the-module-graph/worked-provider.md) — the count, the worked
- provider, and the dashboard case.
-- [ADR 0004](0004-a-node-and-how-it-joins.md) — why asking or subscribing is derived.
diff --git a/02-DECISIONS/0009-modules-and-the-graph.md b/02-DECISIONS/0009-modules-and-the-graph.md
deleted file mode 100644
index 44b6f96f..00000000
--- a/02-DECISIONS/0009-modules-and-the-graph.md
+++ /dev/null
@@ -1,451 +0,0 @@
----
-topic: what runs on it
-status: accepted
-date: 2026-08-28
-deciders: jochen
-reconstructed: false
----
-
-# 9. Modules and the graph
-
-> **Superseded in part — 2026-10-06, by [ADR 0226](0226-the-private-network-is-assigned-by-its-own-name-and-the-proxy-names-its-public-issuer.md).**
-> The section *what a domain module turns out to be* no longer describes what runs: `networking` is retired,
-> every machine is assigned the private network's own module, and the catalogue holds no domain module. The
-> mechanism, a module with requirements and no files, and the rest of this record stand as decided here.
-
-*Consolidated 2026-08-28 from six records.*
-
-## Everything is a module
-
-One kind of thing, one manifest describing all of them. A database, a web application, a window
-manager and a firewall rule set are all modules — not because they are alike, but because
-**anything else means a second kind of thing with its own rules, and then a third.**
-
-**A module is the unit of delivery**: assignable to a node, versionable, replaceable on its own.
-
-## There are no domain modules
-
-An earlier decision grouped modules by domain — four things constituting *how a node is
-reachable* becoming one `networking` module. **That was wrong, and the correction is worth
-keeping** because the observation behind it was right.
-
-The measurement holds: reachability is the **only** place in the catalogue where modules
-genuinely change together under one intent. What did not hold is the conclusion. Tight coupling
-means they share an **authority** — one place that decides for all of them — and not that they
-should be one artifact. `wireguard` and the proxy are deployed to different sets of nodes, so a
-module containing both would be assigned where half of it is unwanted.
-
-> **Coherence is a context. Delivery is a module.**
-
-**Folders assert relationships; edges record them.** What grouping was for — finding things,
-seeing what belongs together — is a tag and a query, neither of which anybody has to keep true by
-hand.
-
-### What a domain module turns out to be, and why it is not the one refused above
-
-> **Superseded in part — 2026-10-06, by [ADR 0226](0226-the-private-network-is-assigned-by-its-own-name-and-the-proxy-names-its-public-issuer.md).** The catalogue no longer
-> holds a domain module: `networking` was retired, and every machine is assigned the private network's
-> own module. A module with requirements and no files stays a thing a module may be; "why it is worth
-> having" below describes what this record decided for `networking`, not what runs.
-
-*Written 2026-08-29, from building it. The heading above reads as a contradiction of what now
-exists and is not one — but only if the difference is stated, so it is stated here.*
-
-**What was refused contains things. What exists contains nothing.**
-
-| | `networking` as refused | `networking` as built |
-|---|---|---|
-| what is in it | WireGuard, a proxy, a firewall — artifacts | nothing at all |
-| what it says | *these ship together* | *I want a private network and names* |
-| what is assigned | one module, half of it unwanted | whatever answers each requirement, each on its own |
-
-The objection above is untouched by this and still correct: a module holding WireGuard and a
-proxy is assigned where half of it is unwanted. **A module holding nothing cannot be, because
-there is no half.** It is requirements and a name, and every artifact it leads to is still an
-ordinary module assigned on its own terms.
-
-**Why it is worth having.** Most people want the network working and do not want to choose a VPN.
-`assign networking` finds one answer to each requirement and takes it without asking, because
-with one candidate there was never a question — the rule below about refusing does the work.
-Somebody who does care assigns the VPN they want, and *that is the whole of choosing*: there is no
-flavor field, no variant syntax, and no second verb. **Picking an implementation is assigning a
-module.**
-
-**What it costs, stated because it is real.** Adding a second implementation to the catalogue
-turns a settled question into an open one for **everyone using the bundle**, not only for whoever
-wanted the alternative. Every node assigned `networking` refuses until somebody says which. That
-is [the refusing rule](#a-requirement-with-several-answers-is-refused-never-guessed) applied
-consistently, and the alternative is a default — which is the flavor field returning under a
-better name. The cost is one assignment per node, and the message names the candidates.
-
-**A consequence that had to be found by running it.** A bundle can drag an implementation in
-through a requirement nobody looked at. Choosing a different VPN still installed WireGuard,
-because the names module needed addresses only WireGuard hands out, and nobody was told. Two VPNs
-on one machine is not always wrong — a machine may run one for another purpose — but being **the**
-network the mesh runs over is singular, so that is a claim, and the collision is refused by name.
-**The general rule: what a bundle pulls in is only as safe as the claims on what it pulls in
-from.**
-
-## Three edges
-
-| edge | means | declared? | satisfied |
-|---|---|---|---|
-| **presence** | that thing must exist and be reachable here | yes | at provisioning |
-| **instantiation** | that thing makes something for me and hands back credentials — a database, a bucket, a route | yes | at provisioning, and again whenever it must be |
-| **build** | I was compiled against that artifact | **no — read from imports** | **at build, once** |
-
-**Instantiation implies presence; presence does not imply instantiation.**
-
-**A route is an instantiation edge**, and it is worth noticing because the direction is the mirror
-of a database: the consumer supplies a target and receives a *name*, rather than supplying nothing
-and receiving credentials. Same edge.
-
-**Provider stops being a category.** Any hosted thing can be a factory — an identity provider
-grants clients, a mail server grants mailboxes. It is a facet, not a kind.
-
-**A module may also declare what it claims**, because some things cannot coexist and that is a
-fact about the module rather than about a particular node. What that means precisely is below.
-
-### Where the answer to a requirement is allowed to live
-
-*Written 2026-08-29, from building it. The table above distinguishes **presence** from
-**instantiation** and this is the half of that distinction nobody had noticed was missing: not
-what the edge hands over, but **where the thing on the other end is.***
-
-Two different things were both being written as a requirement:
-
-| | *a shell*, *a display server*, *a private network* | *a database*, *an object store*, *an identity provider* |
-|---|---|---|
-| where the answer lives | **this machine** | **somewhere in the mesh** |
-| how it is answered | install another module here | find the node already running it |
-| what is missing if absent | a module to assign here | **a decision about where**, which is nobody's to make silently |
-
-Answering the second like the first installs a database on every machine that uses one, which is
-what it did.
-
-**So a provided name carries a scope**, the same idea a claim already has, and written short in
-the ordinary case so the few that are not node-scoped stand out rather than drowning. Scope is a
-property of **the name, not of each provider**: two modules disagreeing about whether a database
-is local would make one requirement mean two things depending on which happened to answer it, so
-that is refused.
-
-**A requirement answered from the mesh is never satisfied by installing it here.** Nothing, and
-the mesh refuses and says which module to assign somewhere. Two, and it refuses and says how to
-choose — the same rule as everywhere else, for the same reason: picking is guessing, and the wrong
-guess puts somebody's data on a machine they did not choose.
-
-**Choosing is recorded per node**, because that is the granularity the choice actually has — two
-machines may reasonably use two different databases and a mesh-wide answer could not say so. A
-choice pointing at a machine that does not provide the thing is refused rather than quietly
-replaced by one that does, and a single available provider does not override a choice either.
-**Both are the same rule: the mesh does not overrule a person, and it does not move data without
-being told to.**
-
-**What this is a prerequisite for.** Knowing *which node* answers is the first half of handing a
-credential back — you cannot be given a database's password before it is settled whose database it
-is. So a node's resolution now records what it takes from elsewhere, which is both the only part
-of its set that stops working when a *different* machine goes away, and the place a credential
-will hang.
-
-### An edge has two directions, and only one of them is built
-
-*Written 2026-08-29, from building it. The row above already says a consumer **supplies a target
-and receives a name**; what it did not say is that those are two separate mechanisms, and that
-having one without the other is what forced two modules outside the system entirely.*
-
-| direction | the consumer says | who needs it |
-|---|---|---|
-| **contribution** | *publish me at this name, on this port* | the proxy, the DNS server, a firewall |
-| **binding** | *and give me back a credential to it* | the database, the object store, the identity provider |
-
-**Contribution is built.** A module declares what it contributes to a requirement; the control
-plane collects every contribution on a node and writes them to a path the provider named, as a
-file, in the mesh's own shape. **Contributing to something is requiring it** — asking to be
-published means a publisher must exist, and a module that had to say both would eventually say
-one, with the failure appearing as a machine where nothing serves the route.
-
-**The control plane does not know what a reverse proxy is**, and does not write one's
-configuration. It delivers the facts; the module turns them into whatever it runs. That boundary
-is what makes swapping the proxy cost nothing in any module that publishes through it, and it is
-[the same separation](0001-mesh-brokers-nodes-host-agents-think.md) that keeps third-party
-software *on* the mesh rather than *of* it. It also costs the host nothing: a received file is a
-file, which was checked by putting the control plane's output through the host's own parser rather
-than by asserting it.
-
-**Binding is built except for the secret**, and that turned out to be the useful way to cut it.
-
-A provider says what a consumer needs in order to use it — a port, a driver, a realm — and a
-consumer says where it wants to be told. The mesh adds the half only it has: **which machine, and
-what that machine is called on the private network.** So an application on one node is handed the
-address of its database on another, as a file, and reaches it by a name the mesh also created.
-
-**The file states that it carries no credential, and why.** A missing field looks like a bug; a
-stated absence looks like a boundary, and somebody wiring this up should not spend an afternoon
-looking for a password that was never going to be there.
-
-### And the secret, which is delivered without ever being held
-
-*Written 2026-08-30, after looking at how the existing mesh does it. The design here is a reaction
-to a measurement, not a preference.*
-
-**The obvious arrangement is a credentials column, encrypted at rest.** It exists, and its own
-tooling records what it bought:
-
-| | |
-|---|---|
-| the tool for finding a secret matches **by value**, not by name | because one password is in the provisions table, the environment table, each node's environment file in plain text, and **inside every connection string composed from it** — copies its documentation calls *"often the only copies actually in use"* |
-| a query against the encrypted column **returns zero rows and proves nothing** | so auditing moved to the decrypted copies on the machines |
-
-**Two faults, and encryption at rest addresses neither.** The control plane can read what it
-stores, so a copy of its database is a copy of every credential in the mesh. And one secret has
-many homes with nothing tracking them — **composition is what mints the untracked ones**, because
-building a connection string centrally creates a new secret-bearing value no rotation path knows
-about.
-
-**So the value is sealed to the node that will use it before it is stored.** With a key that node
-generated and whose private half the mesh has never seen — a third key beside the identity and the
-overlay, for the same reason those are two rather than one. What is stored is unusable by whoever
-holds it, the mesh included, and the broker relays a blob it cannot read. This is what makes
-[ADR 0004](0004-a-node-and-how-it-joins.md)'s *compromise of a node is compromise of that node*
-true of secrets rather than true of identity and quietly false of everything that matters.
-
-**And nothing is composed centrally.** A connection string is assembled on the machine that needs
-one, if at all. The mesh delivers parts.
-
-**What it costs, stated because it is real:** the mesh cannot audit by value. That is the right
-trade rather than an oversight — a query over an encrypted column could not either, so the audit
-was never real. What *is* answerable is which node holds what, which is the question rotation
-actually asks.
-
-**A consequence that shapes the mechanism.** The mesh discarded the plaintext, so it cannot
-compose a file containing it. The credential is therefore **its own file**, holding the value and
-nothing else, beside the readable one. That is better than the alternative it was forced into:
-the readable half stays readable in the declaration, and the secret half changes only when the
-secret does, so a service reloading on it reloads for a real reason.
-
-**Rotation is generating a new one**, because reading the old one back is not possible. Both ends
-are re-sealed and reach their machines in the same push — which removes the window where half the
-mesh holds a dead credential, the failure
-[recorded in ADR 0001](0001-mesh-brokers-nodes-host-agents-think.md) as consumers on three nodes
-holding one for two days.
-
-### The provisioner, which is where the mesh stops
-
-*Written 2026-08-30, from building one and running it against a real database.*
-
-**A password nothing was told to create authenticates nowhere.** The mesh generates one, seals it
-to both ends and cannot read it — so it cannot tell the software to start accepting it either.
-Something on the providing machine reads what arrived and makes it true. That is a provisioner.
-
-**It belongs to the module, not to the mesh**, and the boundary is the same one that keeps
-third-party software running *on* the mesh rather than being *of* it
-([ADR 0001](0001-mesh-brokers-nodes-host-agents-think.md)). The control plane decides and never
-touches a machine. **What the mesh owns is the contract**, which is two files the host writes from
-an ordinary declaration:
-
-| | |
-|---|---|
-| the manifest | every consumer, what it asked for, and **where** its credential is |
-| one file per consumer | that credential, alone in it |
-
-Two files because the mesh discarded the value and cannot compose a document containing it. As
-before, the constraint produces the better shape: the readable half stays readable and auditable
-in the declaration, and the secret half changes only when the secret does.
-
-**It reconciles; it is never told what changed.** It runs after every declaration and must reach
-the same state from wherever it starts. Three consequences, and each of them is a fault that has
-been shipped somewhere:
-
-- **the password is set every time, not only on creation** — otherwise the role already exists,
- nothing happens, and a rotation reports success while changing nothing
-- **what it made and nobody asks for any more is removed** — otherwise a consumer that left keeps
- a working login for ever and nothing ever says so. This is the same rule the host follows about
- [removing what it declared and no longer declares](../04-ISSUES/010-the-first-declaration-destroys-the-substrate/00-report.md)
-- **what it did not make is left alone** — otherwise it cannot be run on a system that predates
- it, which is every system anybody would want to adopt
-
-**A missing credential is refused rather than worked around.** A role created without one is a
-login nothing can use, and nothing would report it until something tried to connect.
-
-**This is where the mesh stops**, and saying so is the point of the section. It decides, delivers
-and can prove what it delivered; the last inch belongs to whoever knows what `create role` means.
-
-**One check that only became possible now.** Two machines wired together across no private network
-is a mesh that reports itself configured and does not work, and the failure surfaces as a
-connection timing out — the slowest place to find anything. It is refused, and it is only
-*checkable* because the network became [something a machine is
-given](#what-a-domain-module-turns-out-to-be-and-why-it-is-not-the-one-refused-above) rather than
-something it has by virtue of holding an address.
-
-**What the absence cost, measured.** Exactly two modules opened a direct connection to the control
-plane's database — the proxy and the VPN — and they are the reason every node permanently holds a
-credential to it. Both were doing by hand what this edge is for. The VPN's half is closed by being
-[a module whose files are computed](../03-DESIGN/01-to-be/08-connectivity.md); the proxy's is
-closed by contribution. **Neither needed a new kind of thing, and both had been outside the model
-for as long as there was one.**
-
-### Why the build edge is a different kind
-
-It is fixed inside an artifact rather than negotiated when something runs, and **its only remedy
-is a rebuild** — nothing can re-provision it.
-
-It is also **derived rather than declared**, and the asymmetry is deliberate: a runtime edge is an
-*intention* somebody has about how the mesh should be wired, and only a person can state it. A
-build edge is a *fact about code that already exists*, and a declared list of dependencies drifts
-from the imports it describes.
-
-**An artifact is out of date when its source moved, or when anything it was built against moved.**
-So what is recorded is a commit *and the identity of every artifact it was built against*, which
-is what makes the rebuild set computable and *is this current?* answerable without building.
-
-**The graph measures design quality, not just build order.** A module with many inbound build
-edges is one whose every change is expensive — and that is readable before anything is built. The
-current shared library is exactly that, and nobody could see it because nothing drew the edges.
-
-## Provisioning is declared, never configured by hand
-
-A module declares what it **provides** and what it **requires**. The mesh satisfies it: a
-provisioner belonging to the provider creates the resource and its credential, records the grant,
-and the values are derived onto the consumer. **Neither the credential nor the topology is ever
-written by hand.** A requirement may name a provider on another node, so cross-node wiring is the
-same declaration.
-
-## What a module claims, and why it is not a list of rivals
-
-*Written 2026-08-29, replacing pairwise exclusion.*
-
-**Exclusivity is not a property of a module. It is a property of a singular resource the module
-takes over.** Two shells do not compete for anything and any number may be installed. Two display
-servers both want the seat, and only one may have it.
-
-> **A module declares what it *claims*. Two modules claiming the same thing cannot both be
-> assigned within that claim's scope.**
-
-**Not "xorg conflicts with wayland".** Pairwise exclusion has a property that only shows up later:
-adding a third display server means **editing xorg and wayland to know about it**. Every new
-module requires changing modules nobody who wrote it owns, and the edits grow as the square of
-the count. With a claim, the third one says `claims: the seat` and nothing else changes anywhere.
-**The new module is the only thing that has to know anything** — which is the difference between
-a catalogue that grows and one that calcifies.
-
-The pattern is common enough to be worth listing, because seeing it is most of understanding it:
-
-| these coexist | these claim one thing |
-|---|---|
-| shells — bash, zsh, fish | display servers — xorg, wayland (*the seat*) |
-| editors — vim, emacs, helix | init — systemd, openrc (*pid 1*) |
-| language runtimes | container runtime — docker, podman |
-| terminal emulators | reverse proxies — nginx, caddy, traefik (*ports 80/443*) |
-| browsers | time — chrony, timesyncd, ntpd (*the clock*) |
-| | resolvers — resolved, dnsmasq, unbound (*`/etc/resolv.conf`*) |
-| | network management — NetworkManager, networkd, netctl |
-| | mail — postfix, exim, msmtp (*port 25*) |
-| | audio — pipewire, pulseaudio (*the device*) |
-
-**A claim has a scope**, because not everything singular is singular per machine:
-
-| scope | example |
-|---|---|
-| **node** | the seat, pid 1, port 443 |
-| **site** | a DHCP server on a segment |
-| **mesh** | the hub, the control plane |
-
-The last is not new — the mesh already enforces exactly one hub with a unique index
-([ADR 0007](0007-connectivity.md)). Scope is that idea, said once rather than hard-coded per case.
-
-**Some conflicts need no claim at all.** Two modules declaring the same file, or binding the same
-port, are visible from *what they declare* — the mesh already holds every resource of every
-declaration. So a claim is only written for the abstract ones, where nothing in the declaration
-reveals the clash. That keeps the manifest small, which is worth protecting.
-
-## A requirement with several answers is refused, never guessed
-
-A module requiring *a shell* may be satisfied by three. The mesh does not pick.
-
-| candidates | what happens |
-|---|---|
-| exactly one | assigned, silently — there was no choice to make |
-| none | refused, naming what is missing |
-| several | **refused, naming them**, and a person chooses |
-
-**This is what makes a solver unnecessary.** Counting candidates is a few lines and has no
-surprising behaviour; a solver that picks has to be understood before its answer can be trusted,
-and it is understood by whoever is debugging it at the time. Nothing here is lost by waiting —
-a solver can be added later without changing a single manifest, and the reverse is not true.
-
-**Requiring a module and requiring a capability are different fields**, because the remedies
-differ and the message should say which:
-
-- *i3 needs xorg, which is not assigned here* — assign it.
-- *this machine has no seat* — wrong machine; nothing can be installed to fix it.
-
-### A capability may carry a value, and that is not a new idea
-
-A capability is a named fact about a machine, **detected and never assumed**. Its presence gates
-an assignment; its detail can also carry a value — `seat: card1-DP-1`, `panel: oled`, an
-architecture, an amount of memory. Nothing new is needed for that: a verdict has always had a
-detail beside its yes or no.
-
-So *can this run here* and *what should it be configured as* are answered by the same fact, read
-two ways. A module that must not be assigned without an OLED panel and one that dims itself
-differently on one are reading the same line.
-
-**What keeps the set from sprawling is the cost of adding one.** A capability must be detected,
-and the detector must say how it knows — so nobody can add one they cannot check, which is the
-whole of [04-ISSUES/007](../04-ISSUES/007-an-installed-package-is-not-a-capability/00-report.md):
-an installed package was treated as a capability and a node was assigned work it could not do.
-
-**And detectors ship inside the host**, which is one statically linked binary. Adding a capability
-means shipping a new host to every node that needs it. That is a real cost and it argues for
-keeping the vocabulary small and general — `seat`, not `has-nvidia-with-two-outputs`.
-
-## "Flavor" is retired
-
-It was carrying three unrelated meanings — variants of a thing, a subset of one module a node
-installs, and whatever the current system does, which earned two knowledge-base entries about
-going wrong. **A word with three meanings cannot be reasoned about**, and every attempt to design
-around it produced a rule that was right for one meaning and wrong for the others.
-
-What it was reaching for is two ordinary things:
-
-- **Different modules that provide the same thing.** `zsh` and `fish` both provide *a shell*. They
- are two modules, not one module with a switch: they share a name and nothing else — different
- packages, different configuration, different everything.
-- **One module with a setting.** A monitoring module that is an agent here and a server there is
- one module, configured. Nothing varies but a value.
-
-If something is neither, it is probably two modules.
-
-**A third thing it was reaching for, added 2026-08-29:** *I want this working and I do not care
-which one.* That is a module with requirements and no files —
-[a domain module](#what-a-domain-module-turns-out-to-be-and-why-it-is-not-the-one-refused-above) —
-and it is what makes "different modules that provide the same thing" bearable for somebody who
-does not want to know there is a choice.
-
-## The core library is the mesh's domain
-
-One module everything may depend on. It holds **what is true of the mesh regardless of which
-context you are in**: a module, a node, an assignment.
-
-The test: *would this still mean the same thing in a context that had never heard of the one it
-came from?* A node would. A pipeline stage would not — that is delivery's.
-
-**Types ship with the module that owns them**, not here. A consumer needing `inventory`'s types
-depends on `inventory` — one narrow, visible edge — rather than everything depending on a hub
-where the relationship cannot be seen. **A library everything depends on is expensive to change
-whether it holds types or code; the fan-in is what makes it expensive**, which is why *types, not
-behaviour* was the wrong guard.
-
-**It stays small on its own.** A domain model changes when what the mesh *is* changes, which is
-rare. A drawer labelled *shared* changes whenever anybody writes something reusable, which is
-constantly — and *who else might want this* always answers yes, which is how the current one grew.
-
-## Consequences
-
-- **Fewer things will be shared, and some code will be written twice.** That is the trade: the
- current library exists because sharing felt free. Two similar functions in two modules is often
- the better answer.
-- **The check is a measurement rather than a prohibition.** Inbound build edges say when something
- is becoming a hub, while it is happening rather than after.
-- **Reading build edges needs a language-aware tool per language**, which is the real cost and the
- reason declaring them looks tempting. It is still wrong.
diff --git a/02-DECISIONS/0010-delivery.md b/02-DECISIONS/0010-delivery.md
deleted file mode 100644
index adf3f97f..00000000
--- a/02-DECISIONS/0010-delivery.md
+++ /dev/null
@@ -1,161 +0,0 @@
----
-topic: what runs on it
-status: accepted
-date: 2026-08-28
-deciders: jochen
-reconstructed: false
----
-
-# 10. Delivery
-
-*Consolidated 2026-08-28 from five records.*
-
-## Delivery is a comparison, not a pipeline
-
-**The control plane holds what source exists and what has been built from it, and builds the
-difference.** A change becomes a build because source is **ahead of artifacts** — answerable at
-any moment — rather than because a message arrived.
-
-**An event makes it fast. Nothing makes it necessary.** A missed notification costs latency and
-cannot cost correctness.
-
-That is the same shape the host uses on a machine, one layer up:
-
-| | reconciles | against |
-|---|---|---|
-| the control plane | artifacts | source |
-| the host | machine state | declarations |
-
-**This is not the current coordinator repaired.** That is a state machine over stages; the value
-of it here is as a catalogue of the ways this fails, and it has been used for exactly that.
-
-### The module system is the CI/CD
-
-*Written 2026-08-29, because this was the intention throughout and was never stated in one line.*
-
-**There is no pipeline product beside the mesh, and there is not going to be one.** A module
-declares what it is ([ADR 0009](0009-modules-and-the-graph.md)); the control plane notices its
-source is ahead of its artifacts and builds it; the graph says what else that invalidates; the
-node that should run it is told. Build, test, publish and deploy are the same reconciliation seen
-at four points, not four stages wired together.
-
-**Which is why the module system is the core of the setup rather than one component of it.** Every
-other layer is carried by it: the substrate is modules the bundle raises before there is a mesh,
-the control plane is a module, and an application is a module with a different manifest. A thing
-that cannot be expressed as a module cannot be delivered at all — that is a real constraint, and it
-is the one keeping a second delivery mechanism from growing beside this one.
-
-**What disappears is the pipeline as a state machine** — no stage list something can be omitted
-from, which is how a verify stage was built and never scheduled, and no run to lose.
-
-### Currency is the whole input closure
-
-**An artifact is out of date when its source moved, or anything it was built against moved.** So a
-shared library changing invalidates everything with a transitive build edge to it, in dependency
-order, because a module cannot be built against a new library until it exists.
-
-**The module graph is a prerequisite of this, not an enabler of it.** Without it there is no
-rebuild set and no ordering, and this cannot be implemented.
-
-## An artifact is build output, never a source tree
-
-Compiled and bundled with its dependency graph inlined. **A deploy is extract-and-run and touches
-no network.**
-
-The consequence is the whole cost of the decision: **anything not in the build output does not
-ship.** Every file kind had to be brought into that rule separately, and each was discovered by
-something silently not happening after a deploy — migrations reading a source layout,
-provisioning scripts reading a source layout, selection files never packaged at all.
-
-## Three silos, and the third is not a stage
-
-The cardinality observation holds and is what the split is for:
-
-| silo | runs | ends with |
-|---|---|---|
-| **build** | once per module | a self-contained artifact |
-| **publish** | once per module | that artifact addressable — an image by digest, a package in the mesh's repository |
-| **deploy** | **once, not once per node** | the affected nodes' **declarations updated** |
-
-**Deploy stops sending commands to nodes.** It changes what the control plane says each node
-should be, which is one write. What happens on the machines is the host's ordinary reconcile.
-
-**Why this fixes the failure class rather than patching it.** Every recorded fault shares one
-shape: *the thing that reported success was not the thing that did the work.* A coordinator
-dispatching a command can only report on dispatch. Under this the reporter **is** the applier —
-which already refuses to record a resource until it read it back, and already fails the whole
-apply on one failed step.
-
-**The verify stage disappears as a stage**, which is the strongest evidence for the shape:
-verification stops being a step that can be omitted from a list and becomes a property of applying
-at all.
-
-**There is no fan-out**, so the defect class that came from the build node having passed through
-two silos while others had not cannot arise.
-
-## A step that fails must fail the job
-
-A step that fails and lets the job continue **reports success for work that did not happen**.
-Absence of an error is not evidence of an effect.
-
-This is the mesh's most consistent failure shape, and it is not incidental — it is what stage
-reporting measured. Documented instances: a service reported started when the container command
-merely returned; an image pull failure that did not fail the deploy; a package install that 404'd
-from every mirror while the job went green; a node left on old code after a failed download with
-a version marker that had already advanced.
-
-## The verdict is tiered
-
-An artifact may not be declared until something has judged it fit. **Two tiers, because one gate
-would be both slow and unreliable:**
-
-| | judged by | when |
-|---|---|---|
-| the module's own tests | the build | **always** — this is most of it |
-| the lab | a raised scenario | when an assertion genuinely needs a mesh |
-
-A lab scenario takes tens of seconds and can fail for reasons that have nothing to do with the
-artifact, and a shared-library change produces a cascade of dozens. One expensive
-non-deterministic gate fails in both directions: a flaky run marks a good artifact unfit, a lucky
-one marks a bad artifact fit, and **neither failure looks like itself.**
-
-**A run that failed environmentally is not a verdict.** A machine that would not boot says nothing
-about the artifact, and recording it as *unfit* is the same untruth as recording a dispatch as a
-deploy.
-
-## What a result means
-
-> **The declaration is updated, and here is which nodes have applied it.**
-
-A pipeline does not wait for every node — one may be legitimately switched off for a week, and a
-delivery mechanism that blocks on a sleeping laptop is one nobody will use.
-
-```
-delivered declaration updated for 5 nodes
-applied 3 of 5
-outstanding 2 — last seen 4 days ago, 20 minutes ago
-```
-
-**Outstanding is not failure**, and conflating them is how the old system produced a stall with no
-error anywhere.
-
-## What must exist first
-
-1. **The module graph, with build edges.** No graph, no rebuild set and no ordering.
-2. **A recorded input closure per artifact**, so currency is answerable without building.
-3. **Something that notices a reconciler is not converging.** Below.
-
-## Open, and the first is the real risk
-
-- **A loop that will not converge is harder to debug than a job that failed.** A failed job stops
- and names its step; a reconciler retries forever. Without something that notices *this has been
- trying for an hour*, the failure is **silence** — the fault this removes, reintroduced in a new
- place.
-- **The run identity people use is lost.** *Did my change go out?* is answerable today by opening
- a pipeline. Something must replace that or this is worse to live with, whatever its properties.
-- **Does a fit artifact declare itself?** If it does, merging to main deploys to production —
- which may be wanted and is far too large a property to acquire by omission.
-- **Rebuild storms are mostly behaviourally empty.** Reproducible builds would stop a cascade at
- the first module whose output did not move; without them one commit redeploys the fleet for no
- change in behaviour.
-- **Detection stays the fragile input for latency**, though no longer for correctness.
diff --git a/02-DECISIONS/0011-managed-files-are-generated-never-edited.md b/02-DECISIONS/0011-managed-files-are-generated-never-edited.md
deleted file mode 100644
index 6bcb1cd6..00000000
--- a/02-DECISIONS/0011-managed-files-are-generated-never-edited.md
+++ /dev/null
@@ -1,64 +0,0 @@
----
-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`.
diff --git a/02-DECISIONS/0012-the-mesh-creates-no-symlinks.md b/02-DECISIONS/0012-the-mesh-creates-no-symlinks.md
deleted file mode 100644
index 6d513a6d..00000000
--- a/02-DECISIONS/0012-the-mesh-creates-no-symlinks.md
+++ /dev/null
@@ -1,101 +0,0 @@
----
-topic: building it
-status: accepted
-date: 2026-08-23
-deciders: jochen
-reconstructed: false
----
-
-# 12. The mesh creates no symlinks — a derived file is a copy
-
-## Context
-
-[ADR 0012](0012-the-mesh-creates-no-symlinks.md) responded to production data loss — a hand-made
-link, resolved through a container engine's volume handling, pointing a mount somewhere it
-should not have — by centralising linking in the installer and forbidding it everywhere else.
-
-That narrowed the incident class. It did not close it. The hazard is not *who* made the link;
-it is that a path can resolve somewhere other than where it appears to. A link made by the
-installer resolves exactly the same way as a link made by hand. The rule made the mechanism
-rarer and better-governed while leaving the mechanism in place.
-
-Two things have changed since, and together they remove the argument that kept it.
-
-**The original case for linking was staleness.** A copy of a service definition goes stale
-silently while the catalogue moves on, so a link was the cheap way to guarantee the running
-node reads a current definition. That argument assumes the node's copy is unmanaged.
-
-**It is not.** [ADR 0011](0011-managed-files-are-generated-never-edited.md) established that
-everything on a node's disk is derived from the mesh and regenerated when its inputs change,
-and the installer already **reconciles** links rather than assuming them — repointing stale
-ones, adopting real files it finds where a link belongs. Reconciling content is the same
-operation as reconciling a pointer, plus a comparison.
-
-So the mesh already has the machinery that makes a copy safe, and is using a link to solve a
-problem that machinery solves better. Worse, a link is conceptually the wrong shape: it makes
-the node's runtime state a *pointer into source*, which is the one thing
-[ADR 0006](0006-the-substrate-and-the-control-plane.md) and ADR 0011 exist to prevent.
-State is derived onto nodes; it does not reach back.
-
-## Considered options
-
-1. **Keep ADR 0019 as the final position** — centralised linking, forbidden elsewhere.
- Rejected as the status quo. It governs the mechanism rather than removing it, and the
- failure it was written for remains reachable by any code path the installer trusts.
-2. **Keep links but harden them** — canonicalise before mounting, refuse a link that escapes
- an expected root. Rejected: it is a check bolted onto a hazard, and it has to be correct in
- every consumer, including container engines the mesh does not control.
-3. **Copy, reconciled by the installer, with staleness detected rather than assumed away.**
- Proposed here.
-
-## Decision
-
-*Accepted 2026-08-25. The position is settled; the migration is not designed. See "Open" below.*
-
-**The mesh creates no symlinks.** A file a node needs is placed on that node as a real file,
-derived from the mesh and reconciled by the installer like every other managed file
-([ADR 0011](0011-managed-files-are-generated-never-edited.md)).
-
-The prohibition in ADR 0019 stands and widens: it ceases to be "only the installer may link"
-and becomes "nothing links, the installer included".
-
-When this is accepted, ADR 0019 becomes superseded rather than edited — its reasoning is why
-the rule exists at all, and the incident behind it is the reason anyone believes either record.
-
-## Consequences
-
-- The path-resolution hazard is removed rather than governed. There is no link for a container
- engine to resolve, so the class of failure that cost production data is closed by
- construction.
-- A node's runtime state stops pointing into source. What a node holds is derived output, which
- is what the mesh's model already says it is everywhere else.
-- **Staleness becomes a real problem that must be answered, not assumed away.** This is the
- cost, and it is the whole cost: today a link cannot be stale, and a copy can. The answer has
- to be detection — the installer comparing what is on disk against what the mesh says should
- be — and it must be loud, because a silently stale definition is exactly the failure shape
- this mesh keeps producing ([ADR 0010](0010-delivery.md)).
-- Reconciliation gets more expensive: comparing content rather than checking a pointer's
- target, on every module, on every node.
-- Disk usage rises, trivially, and is not a consideration.
-- Existing links must be converted. A node mid-migration holds both forms, so reconciliation
- has to handle finding a link where a file now belongs — the mirror image of the adoption it
- already does.
-
-## Open
-
-- **How staleness is detected.** Content hash, version marker, or regeneration on every
- reconcile. This is the decision that makes or breaks the change and it is not taken here.
-- **Whether anything must keep a link** for reasons outside the mesh's control. If something
- does, that is a finding worth recording rather than an exception worth granting quietly.
-- **Migration order.** Converting a node's links is a change to how its services resolve their
- own definitions, which is not a change to make everywhere at once.
-
-Until those are answered this record stays `proposed`, and ADR 0019 remains the governing rule.
-
-## References
-
-- [ADR 0012](0012-the-mesh-creates-no-symlinks.md) — the incident, and the rule this widens.
-- [ADR 0011](0011-managed-files-are-generated-never-edited.md) — the machinery that makes a
- copy safe.
-- [`03-DESIGN/00-as-is/05-runtime-and-installation.md`](../03-DESIGN/00-as-is/05-runtime-and-installation.md)
- — what the installer does today, including reconciliation and adoption.
diff --git a/02-DECISIONS/0013-schema-changes-are-numbered-migrations.md b/02-DECISIONS/0013-schema-changes-are-numbered-migrations.md
deleted file mode 100644
index cb7519e1..00000000
--- a/02-DECISIONS/0013-schema-changes-are-numbered-migrations.md
+++ /dev/null
@@ -1,69 +0,0 @@
----
-topic: building it
-status: accepted
-date: 2026-05-14
-deciders: jochen
-reconstructed: true
----
-
-# 13. Schema and state changes are numbered migrations, in the same language as the code
-
-> Reconstructed after the fact from the evidence cited below.
-
-## Context
-
-Modules own persistent state. That state has to change as they change, across nodes that are
-at different versions, some of which have data that predates the change.
-
-Two things were being done that do not survive contact with a second node. Schema was created
-at startup, so what a table looked like depended on which version last started. And migrations
-were shell scripts, so they could not use the types, connection handling or helpers the module
-already had, and were not compiled or checked with it.
-
-## Considered options
-
-1. **Startup SQL / create-if-missing.** Rejected. It converges only for a node that started
- with the newest version. A node that never restarts never migrates; a node that restarts on
- an old version can undo a change.
-2. **Shell migrations.** Rejected. Unchecked, untyped, and a separate dialect from the module
- they belong to. Also, as later discovered, packaged differently — and therefore
- occasionally not packaged at all.
-3. **Numbered migrations in the module's own language, compiled with it.** Chosen.
-
-## Decision
-
-Every schema or state change is a numbered migration file, written in the same language as the
-module and compiled with it. There is no startup schema creation and no ad-hoc statement.
-
-Rules that come with it:
-
-- The initial migration is **frozen** once it has run anywhere. It is never modified; a change
- is a new number.
-- Every statement is **idempotent** — guarded so that re-running is safe.
-- A change needs **both** a baseline for a fresh installation and an incremental migration for
- installations that already exist. Code referencing a column requires that the migration
- creating it exists.
-- Migration numbers are unique. A duplicate prefix is a defect, not a style issue.
-
-## Consequences
-
-- A node at any version converges to the current schema by running the migrations it has not
- run.
-- Migrations are checked by the same compiler as the code, and a migration that does not
- compile fails the build rather than the deployment.
-- The rules are enforced unevenly. Duplicate prefixes have shipped repeatedly and been fixed by
- renumbering afterwards; the mesh now validates for them, which is the check this rule needed
- in order to be real.
-- A migration directory is a feature like any other, which means it is packaged like any other
- — and when packaging is wrong, migrations silently do not ship. This has happened.
-- Freezing the initial migration means a fresh installation replays the entire history. That
- cost grows and nothing currently bounds it.
-
-## References
-
-- `fix(agents): convert workflow migrations to TypeScript` (#50), 2026-05-14.
-- `feat(dev_validate): guard against duplicate migration numeric prefixes` (#379), 2026-06-26 —
- the rule acquiring a check.
-- Knowledge base: `migrations/schema-drift`, `noxflow/troubleshooting/migration-number-collision`.
-- Packaging failures: `troubleshooting/shell-migrations-never-packaged`,
- `troubleshooting/provision-migration-never-applied`.
diff --git a/02-DECISIONS/0014-no-npm-workspace.md b/02-DECISIONS/0014-no-npm-workspace.md
deleted file mode 100644
index 1140d375..00000000
--- a/02-DECISIONS/0014-no-npm-workspace.md
+++ /dev/null
@@ -1,66 +0,0 @@
----
-topic: building it
-status: accepted
-date: 2026-06-04
-deciders: jochen
-reconstructed: true
----
-
-# 14. No workspace — each module is a standalone package consuming published dependencies
-
-> Reconstructed after the fact from the evidence cited below.
-
-## Context
-
-Modules depend on each other, above all on the shared library every module builds against.
-A workspace was the obvious way to express that: sibling packages, resolved locally, one
-install at the root.
-
-It produced a divergence that is worth stating precisely, because it is not obvious. In
-development, a workspace member importing a sibling resolves to that sibling's **local source**.
-In the pipeline, each module is built alone, from a clone, without its siblings present — so
-the same import resolves to the **published version**. The two environments were therefore
-building different code from identical source, and the failure appeared only in the pipeline,
-in a module that had not been touched.
-
-## Considered options
-
-1. **Keep the workspace and make the pipeline replicate it** — clone every module, build the
- graph. Rejected: it makes every build a whole-repository build, which is the cost the
- per-module pipeline exists to avoid, and it does not extend to modules in their own
- repositories.
-2. **Keep the workspace and pin siblings to published versions.** Rejected as the worst of
- both: the workspace's local resolution silently overrides the pin, so the divergence
- remains while looking solved.
-3. **No workspace. Every module is standalone and consumes published dependencies.** Chosen.
-
-## Decision
-
-There is no workspace. Each module is an independent package that declares its dependencies
-and consumes them from the private registry, including the mesh's own shared library.
-
-A cross-package change is therefore two steps: publish the producer, then consume it. The
-pipeline does the first on push and resolves the levels so that a module always builds against
-its dependencies' freshly published versions.
-
-## Consequences
-
-- Development and the pipeline resolve imports identically. The divergence is gone by
- construction rather than by discipline.
-- A module in its own repository is not a special case. It builds exactly as a module in the
- monorepo does — which is what makes [ADR 0015](0015-applications-live-in-their-own-repository.md)
- cheap.
-- A cross-package change costs a publish-and-consume round trip. This is the real price, paid
- on every shared-library change.
-- There is no repository-wide install and no repository-wide build. Anything that assumed one
- broke, and one thing that assumed one has stayed broken: the end-to-end pipeline harness has
- not built since this decision landed. See
- [`04-ISSUES/005`](../04-ISSUES/005-pipeline-test-harness-unbuildable/00-report.md).
-
-## References
-
-- `fix(noxflow): kill npm workspace, restore encryption inside PgAdminRepo` (#240),
- 2026-06-04. The reason is recorded in the root package manifest, which still carries the
- note explaining why no workspace exists.
-- The divergence it fixed is named there: workspace members importing each other resolved to
- local unbuilt source in the pipeline.
diff --git a/02-DECISIONS/0015-applications-live-in-their-own-repository.md b/02-DECISIONS/0015-applications-live-in-their-own-repository.md
deleted file mode 100644
index 8ad13968..00000000
--- a/02-DECISIONS/0015-applications-live-in-their-own-repository.md
+++ /dev/null
@@ -1,65 +0,0 @@
----
-topic: building it
-status: accepted
-date: 2026-07-10
-deciders: jochen
-reconstructed: true
----
-
-# 15. Applications live in their own repository; the monorepo is for the mesh
-
-> Reconstructed after the fact from the evidence cited below.
-
-## Context
-
-The module system makes adding anything to the monorepo trivial — a directory and a manifest.
-That ease is the problem. Standalone applications, sites and side-projects accumulated beside
-the mesh's own components, and once there they inherited the monorepo's review cadence, its
-pipeline detection, and its history.
-
-The mesh's own code and an application that merely runs on the mesh have nothing in common
-except the manifest format. They change for different reasons, are reviewed by different
-criteria, and have no reason to share a branch.
-
-## Considered options
-
-1. **Everything in the monorepo.** Rejected — it is what existed. The monorepo becomes an
- inventory of one installation's applications, and every application change queues behind
- mesh review.
-2. **A second monorepo for applications.** Rejected: the same coupling with an extra name.
- Applications have no more in common with each other than with the mesh.
-3. **One repository per application, registered with the mesh as a build source.** Chosen.
-
-## Decision
-
-Every standalone application, site or side-project lives in its own repository, with a manifest
-at the root. It registers with the mesh as a build source and is then built, provisioned,
-deployed and verified by exactly the same pipeline as anything in the monorepo.
-
-The monorepo holds the mesh: the runtime, the core modules, the delivery machinery, and the
-shared infrastructure the mesh itself provisions against.
-
-Creating an application directory in the monorepo is a convention violation, and reviewers
-reject it.
-
-## Consequences
-
-- An application's cadence is its own. It is not reviewed as mesh code and does not queue
- behind mesh work.
-- The separation is safe **only because** the pipeline and provisioning are identical either
- side of it — which [ADR 0014](0014-no-npm-workspace.md) is what makes true. Without
- standalone packages this decision would fork the build.
-- The monorepo stops being an inventory of the installation, which is a precondition for
- publishing anything about it.
-- Discovery gets harder: there is no single listing of everything the mesh runs, and the
- registry of build sources becomes the closest thing to one.
-- A module source that is not registered is silently skipped by the pipeline. The cost of
- being outside the monorepo is that being forgotten is possible.
-
-## References
-
-- The rule is stated in the governed constitution page authored 2026-07-10, §3, as a
- convention violation reviewers must reject.
-- [ADR 0001](0001-mesh-brokers-nodes-host-agents-think.md) decision 4 extends this from *new*
- applications to the modules already in the monorepo.
-- Knowledge base: `troubleshooting/unregistered-module-source`.
diff --git a/02-DECISIONS/0016-the-lab.md b/02-DECISIONS/0016-the-lab.md
deleted file mode 100644
index b4b04f44..00000000
--- a/02-DECISIONS/0016-the-lab.md
+++ /dev/null
@@ -1,92 +0,0 @@
----
-topic: building it
-status: accepted
-date: 2026-08-28
-deciders: jochen
-reconstructed: false
----
-
-# 16. The lab
-
-*Consolidated 2026-08-28 from five records. The lab is one design and was split across five
-decisions taken over three days; the reasoning is kept, the fragmentation is not.*
-
-The environment a change is run against before it reaches real machines.
-
-> **Still the lab, no longer the test bed — 2026-09-30, by [ADR 0149](0149-the-live-mesh-is-the-test-bed.md).**
-> Everything here stands. What changed is what the lab is *for*: a change is verified against the mesh
-> that is running, because the faults that cost the most are faults of a mesh that already exists —
-> bound consumers, containers made against an older roster, an adopted machine — and a bed is by
-> construction a mesh that does not. Raising a mesh from bare is now the lab's whole job, which is the
-> one thing the live mesh cannot be asked to do. 0149 also supersedes
-> [ADR 0068](0068-the-lab-takes-requests.md), which extended this one and was never built.
-
-## A node in the lab is a virtual machine
-
-It boots a stock Linux image, runs the real install, and becomes a node. **It is not a model of
-a node**, so no question arises about how good the model is — which is the whole reason for
-paying the cost of virtual machines rather than containers.
-
-The lab is driven by **incus**, and a scenario is raised from a declaration.
-
-## A router is scenery, and is therefore a container
-
-**Nothing under test runs on a router.** It is not a participant, holds no identity, has nothing
-installed on it by the mesh, and no assertion is ever made about its internals. It exists so that
-packets between machines behave the way they behave in the world.
-
-The fidelity argument that makes a node a virtual machine does not reach it: what a router *is*
-does not matter, only what it *does to traffic*. So a router is a system container, and the lab
-is cheaper for it.
-
-## A scenario declares the underlay, and only the underlay
-
-**What a hosting provider and a home router would have provided**, before any of our software
-touched the machine:
-
-- which segments exist, and their address ranges
-- which machine sits on which segment, at which address
-- what NAT sits between them, and which ports are forwarded through it
-- which machines are detached, and may be attached or detached during a run
-
-**A scenario declares nothing about the overlay** — no overlay addresses, no hub, no peering, no
-names, no certificates. Those are the mesh's job, and a scenario that supplied them would be
-testing itself.
-
-> A scenario provides what a hosting provider and a home router would provide, and nothing our
-> software is responsible for.
-
-## A scenario is a closed address space
-
-Every segment materialises as its own isolated link belonging to one scenario instance. **Two
-scenarios raised from the same declaration hold the same addresses and never meet**, because
-nothing joins their links. The declaration therefore keeps its literal addresses and they mean
-exactly what they say.
-
-**The consequence that constrains everything else: the lab never reaches into a scenario over
-IP.** It talks to a machine through the virtualisation layer's own channel — the way one would
-use a console rather than the network. That is what makes two identical scenarios able to run at
-once, and it is why placing anything inside a machine is a hypervisor operation rather than a
-network one.
-
-## Two scenario classes, and the first has no pipeline
-
-| | **bootstrap** | **full** |
-|---|---|---|
-| contains | machines, the host binary, a pinned substrate bundle | a complete mesh: forge, coordinator, delivery, modules |
-| verdict from | what the host reports about the state it reconciled | a delivery result ending in verification |
-| exercises | tiers 0 and 1 | tiers 2 and above, and modules |
-
-**The bootstrap class comes first**, because it is what develops the node host, and because a
-full scenario needs tiers that do not exist yet. A lab that could only raise the larger class
-would be a lab nobody could use until everything else was built.
-
-## Consequences
-
-- **The lab tests the real code path**, not a reimplementation of it. The network a scenario
- produces is generated by the same code production runs.
-- **Isolation is what makes it usable in parallel**, and it costs the ability to reach in over
- IP. Everything the lab puts inside a machine — a binary, an image, a file — goes through the
- hypervisor.
-- **A sealed scenario cannot fetch anything**, which is a real limit rather than an inconvenience:
- it is why images have to be placed and why a container runtime has to be in the base image.
diff --git a/02-DECISIONS/0017-a-test-defends-a-decision.md b/02-DECISIONS/0017-a-test-defends-a-decision.md
deleted file mode 100644
index 57ce3265..00000000
--- a/02-DECISIONS/0017-a-test-defends-a-decision.md
+++ /dev/null
@@ -1,92 +0,0 @@
----
-topic: checking it
-status: accepted
-date: 2026-08-24
-deciders: jochen
-reconstructed: false
----
-
-# 17. A test defends a decision
-
-## Context
-
-[`how-we-build.md`](../00-META/how-we-build.md) §5 already says that **if a document states a
-rule about the mesh, it says how the rule is verified**, on the grounds that an unenforced rule
-is indistinguishable from a wrong one and costs more, because people believe it.
-
-That rule is applied to prose and to acceptance criteria. It has never been applied to
-**decisions**, and it should be — a decision record states something that must be true, which
-is the same kind of claim.
-
-The gap was found by review. The lab reached 2,128 lines with 1,072 of them untested, and
-**no stated rule was broken.** There is no testing posture in `how-we-build.md` at all: no
-expectation, no gate, no definition of done. Every decision the lab embodies — the underlay
-boundary, the closed address space, routers as scenery, waiting for usable rather than for a
-call to return — was verified by hand, by running scenarios and reading output, and none of
-that survives the terminal it was run in.
-
-Which is the fault the mesh already has catalogued at scale: an end-to-end harness that has
-not built since 2026-06-04, and nothing said so
-([`04-ISSUES/005`](../04-ISSUES/005-pipeline-test-harness-unbuildable/00-report.md)). Coverage
-assumed rather than checked.
-
-## Considered options
-
-1. **A coverage percentage.** Rejected. It measures how much code a test touched, not whether
- anything important is defended, and it is satisfied by tests that assert nothing. A number
- would have been met by testing the parser harder while the hypervisor integration stayed
- unasserted.
-2. **Test-driven development as a hard rule.** Rejected, and not because it is wrong in general.
- Half the lab's implementation was discovery: that the hypervisor CLI reads a definition from
- stdin and hangs, that it assigns a MAC without recording it, that a stock image's boot-time
- networking flushes a static address. A test written first against undiscovered behaviour
- asserts a guess.
-3. **A test defends a decision.** Chosen.
-
-## Decision
-
-**Every decision record states something that must be true. A test asserts it.**
-
-A decision with no test is a decision that will quietly stop being true, and nobody will find
-out from a document. Concretely:
-
-- Where a decision is about **structure or logic** — what a declaration may say, what is
- refused, how a name is derived — the test is a unit test, and it is **written first**, because
- the behaviour is knowable before the code.
-- Where a decision is about **behaviour against a real system** — a hypervisor, a broker, a
- daemon — the test runs against the real thing, and is written **alongside**, because the
- behaviour is discovered rather than known.
-- **Mocking the boundary is forbidden.** A test that fakes a hypervisor asserts that the fake
- behaves as expected, which is the shape of test this whole effort exists to stop shipping.
-- **The gate is blocking, and green is the definition of done.** A change that has not run its
- tests is not finished, whatever its diff looks like.
-
-A test names the decision it defends. Not as ceremony: it is what makes the pairing checkable,
-so a decision without one can be *found* rather than noticed.
-
-## Consequences
-
-- The question *"which tests matter"* has an answer that is not a number. The decisions are the
- list, and they are already written down.
-- **A new decision costs a test.** That is the intended friction — a decision nobody will assert
- is one worth reconsidering.
-- Integration tests need real infrastructure and are slow. That cost is accepted: a fast test
- suite that mocks the boundary would tell us nothing about the boundary, which is where every
- interesting fault in this session actually was.
-- Some decisions are not mechanically assertable — *the mesh brokers capabilities; nodes host;
- agents think* is a shape, not a predicate. Those should say so in the record rather than being
- quietly exempt, so the exemption is visible.
-- Records 0001–0033 were made before this rule. They are not retroactively invalid, but each
- should acquire a test or an explicit note that it cannot have one, and until then this rule
- is aspirational for them — which is exactly the state §5 warns about, recorded rather than
- hidden.
-
-## References
-
-- [`how-we-build.md`](../00-META/how-we-build.md) §5 — the rule this extends from prose to
- decisions.
-- [`04-ISSUES/005`](../04-ISSUES/005-pipeline-test-harness-unbuildable/00-report.md) — coverage
- assumed rather than checked, for two and a half months.
-- The sibling HQ repository for the PAPA platform states the same boundary rule — the contract
- is tested against the real system, mocking the client is forbidden, and a blocking gate is the
- definition of done. This record adopts that posture and adds the decision pairing.
diff --git a/02-DECISIONS/0018-a-picture-is-read-from-what-runs.md b/02-DECISIONS/0018-a-picture-is-read-from-what-runs.md
deleted file mode 100644
index 2316672d..00000000
--- a/02-DECISIONS/0018-a-picture-is-read-from-what-runs.md
+++ /dev/null
@@ -1,99 +0,0 @@
----
-topic: checking it
-status: accepted
-date: 2026-08-24
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0017-a-test-defends-a-decision.md
----
-
-# 18. A picture of a system is read from the system, never from what asked for it
-
-## Context
-
-A scenario declaration is a file. A raised scenario is a set of machines, links and rulesets.
-The two are supposed to correspond, and the entire value of the lab rests on noticing when
-they do not — [ADR 0017](0017-a-test-defends-a-decision.md) says a claim nothing checks is a
-claim that will quietly stop being true.
-
-Drawing a scenario makes that concrete, and forces a choice that looks cosmetic and is not.
-A diagram of a running system can be produced two ways: parse the declaration and lay it out,
-or interrogate the system and lay *that* out. The first is far easier — the declaration is
-already parsed, already validated, already in memory.
-
-It is also worthless for the only question worth asking of such a picture: *is what is running
-what I asked for?* A drawing built from the request and captioned **as raised** answers that
-question with the request, which always agrees with itself.
-
-This is the same fault as
-[04-ISSUES/003](../04-ISSUES/003-firewall-scope-is-read-by-no-code/00-report.md) — a firewall key declared in
-five manifests and read by no code, so a manifest appears to restrict a port and restricts
-nothing. The declaration was never wrong. Nothing ever asked the system.
-
-## Considered options
-
-1. **Draw the declaration, and label it honestly.** Cheap, and useful for review before
- anything is raised. Insufficient alone: it can never disagree with itself.
-2. **Draw the system, inferring the rest from the declaration where the system is silent.**
- The tempting middle. Rejected — a picture where some facts are observed and some are
- assumed has no honest caption, and the assumed ones are exactly the interesting ones.
-3. **Two pictures, one layout, neither borrowing from the other.** Chosen.
-
-## Decision
-
-**A picture captioned *as raised* reads only the running system.** It never opens the
-declaration, not even for a fact the system happens not to record.
-
-Where the hypervisor does not natively hold a fact the picture needs — whether a segment is
-public, what a gateway translates, whether a machine refuses inbound — **the raise records it
-on the resource** as metadata, and the picture reads it back from there.
-
-That recording carries its own rule, which is the substance of this decision rather than an
-implementation note:
-
-> **A behavioural tag is written after the behaviour works, never when the resource is
-> created.**
-
-Written at creation, a tag restates the request in a new location and inherits none of the
-authority of having happened. A failed raise deliberately leaves its wreckage standing, so a
-tag written up front would let a picture of that wreckage badge translation the gateway was
-never configured to do — reproducing, inside the tool built to catch the fault, exactly the
-fault.
-
-So: the gateway is tagged after its ruleset applies; the machine after the read-back proves
-its firewall loaded.
-
-Both pictures render through **one layout**, so they can be put side by side and the
-difference read off directly.
-
-## Consequences
-
-- **It earned itself on the first comparison.** Drawn side by side, every virtual machine in
- the live picture held no addresses at all. A container's interface carries the name of the
- device it was configured as; a virtual machine names its own — so joining addresses to
- devices by name attached every address to a container and none to a VM. Nothing failed;
- a whole class of machine silently lost its addresses. The two pictures disagreed, so it was
- visible in seconds. It is now joined on MAC.
-- Raise does more work, and writes metadata it does not itself consume. Accepted: the cost is
- a few config keys, and it is what makes a raised instance self-describing.
-- A resource raised before a tag existed is missing it. The reader says so rather than filling
- the gap from the declaration — an untagged link draws as unknown, not as what the file said
- it should be.
-- **The rule generalises past diagrams.** Anything reporting on the mesh — a status view, an
- inventory, a health check — is subject to it. A report assembled from the intended state is
- not a report.
-- The declared picture stays, and stays useful: it is review before raising, and it is one half
- of the comparison. It carries no runtime status, because it cannot know any.
-
-- **The constitution sync is now owed.** Accepted 2026-08-25, so §5 carries the rule
- unqualified and playbook [05](../00-META/process/05-constitution-sync.md) is due. Until it
- runs, the mesh does not enforce this — an unsynced rule is a rule the mesh does not enforce,
- whatever this document says.
-
-## References
-
-- [ADR 0017](0017-a-test-defends-a-decision.md) — a claim nothing checks stops being true.
-- [ADR 0016](0016-the-lab.md) — why the lab must not supply what the
- mesh is responsible for; the same instinct, applied to facts rather than to configuration.
-- [04-ISSUES/003](../04-ISSUES/003-firewall-scope-is-read-by-no-code/00-report.md) — the fault in production
- form.
diff --git a/02-DECISIONS/0019-how-this-repository-works.md b/02-DECISIONS/0019-how-this-repository-works.md
deleted file mode 100644
index 6d6e871f..00000000
--- a/02-DECISIONS/0019-how-this-repository-works.md
+++ /dev/null
@@ -1,158 +0,0 @@
----
-topic: how we work
-status: accepted
-date: 2026-08-28
-deciders: jochen
-reconstructed: false
----
-
-# 19. How this repository works
-
-> **Superseded in part — 2026-10-11, by [ADR 0291](0291-an-issue-lives-whole-in-the-issue-tracker.md).**
-> `04-ISSUES` is no longer a part of this repository: an issue lives whole in the issue tracker, its report,
-> diagnosis and resolution included, and the folders are imported there and removed (issue 427). The other three
-> folders, and everything else here, stand.
-
-> **Superseded in part — 2026-10-07, by [ADR 0248](0248-the-decision-index-is-generated-when-it-is-read-and-never-stored.md).**
-> The paragraph "the index is written, not only generated on demand" no longer holds: a stored list was
-> a line every decision pull request added to one file, so two open at once always conflicted (issue
-> 297). The list of records is generated when it is read, and `02-DECISIONS/README.md` writes only the
-> topics in their reading order. Everything else here stands, including the reading order and the check
-> that every record has a topic one of the README's.
-
-*Consolidated 2026-08-28 from ten records that were one decision seen from ten angles. The
-reasoning is kept; the fragmentation is not.*
-
-## The repository
-
-**`novox/hq` is Novox's headquarters, and it is public.**
-
-Company-scoped, not the mesh's. Today almost everything in it is about the mesh, because the
-mesh is what Novox is building — a fact about the present rather than a definition. A second
-product would live here too.
-
-**Public** means written for a reader who is not its author and has no access to the mesh it
-describes. Nothing here may contain routable addresses, real domain names, node names, absolute
-paths, usernames or credentials. The test: *would this paragraph still teach a stranger running
-an entirely different mesh?*
-
-**Separate from the code** because the cadence differs — a decision changes when thinking
-changes, not when code changes — and because a public repository cannot be a private one's
-subdirectory.
-
-**The naming rule:** a repository belonging to a product carries that product's prefix. A
-company-scoped one does not. So this is `hq` and the mesh's are `mesh-*`.
-
-| Repository | Tier | Holds |
-|---|---|---|
-| `novox/mesh-host` | 0 | the node host — the one binary installed by hand |
-| `novox/mesh-substrate` | 1 | the pinned tier-1 services, as declarations |
-| `novox/mesh-control` | 2 | the control plane and its contexts |
-| `novox/mesh-surfaces` | 3 | tools, web, cli — thin, no logic |
-| `novox/mesh-sdk` | — | the mesh's own domain ([ADR 0009](0009-modules-and-the-graph.md)) |
-| `novox/mesh-lab` | — | the lab: scenario lifecycle, networking, placement |
-| `novox/hq` | — | this one |
-
-**The product is `Novox Mesh`**, shortened to `mesh` in internal use — repository names, the
-module namespace, environment variables, paths. **`Nox` is an identity of Novox**, an agent
-participant within the mesh's own model, not a second system.
-
-## The folders, and why they are numbered
-
-**The numbering is the flow.** Research produces a decision; the decision authorises a design.
-Following the numbers walks the process in the order it happens.
-
-| | |
-|---|---|
-| `01-RESEARCH` | an open question, while it is open |
-| `02-DECISIONS` | what was decided, and why |
-| `03-DESIGN` | what is being built |
-| `04-ISSUES` | something wrong at the level of design or governance |
-
-**`03-DESIGN` has two layers and they are never mixed.** `00-as-is/` describes the mesh that
-exists, written from the implementation and the operational record. `01-to-be/` describes the one
-being built toward. Every document says which it is. A statement about the future does not belong
-in an as-is document, and an as-is document is never edited to describe an intention.
-
-**`04-ISSUES` is for design-level faults** — a rule enforced by nothing, a stated invariant that
-is false, a failure the design permits to be silent. Not an operational ticket queue.
-
-## What a decision record is, and is not
-
-**If a decision is worth recording, it is worth a record. If it is not worth a record, it is not
-recorded.**
-
-That bar has been read too generously. A *finding* is not a decision. A bug is not a decision.
-**A record is warranted when there is a genuine fork**: a direction reversed, an alternative
-seriously considered and likely to be proposed again, or something contested that needs to stay
-settled. Everything else belongs in the design document, where the reasoning is read.
-
-**There is no ledger** — no separate document summarising, ranking or tracking decisions. A
-chronological view is generated from frontmatter, which is what a ledger was actually for.
-
-**A number identifies a record and never changes.** It is not a position, and it cannot be
-both — a position moves when the set changes, and an identity that moves is not one.
-
-That is not a preference. Records are referenced from **outside** this repository: code
-comments, commit messages, the knowledge base. Renumbering once cost 96 references across two
-code repositories, and nothing in either would have failed to compile — the comments would
-simply have pointed at the wrong reasoning, which is worse than a broken link because nothing
-reports it.
-
-**So the reading order lives in a generated index**, from each record's `topic:` — what the mesh
-is, then its tiers from the bottom up, then what runs on them and how it gets there, then how it
-is built, how it is checked, and how we work.
-
-**And the index is written, not only generated on demand.** A reader looking at the folder on a
-forge sees the folder, not a command. The objection to a written index is that it drifts, and
-that is answered by **checking** it rather than by refusing to write one — which is §5's own
-rule: a rule states how it is checked. A record with no topic, or a topic nobody defined, fails
-the same check, because the quiet failure is a record that vanishes from the order rather than
-appearing in the wrong place.
-
-**The design layer is what you read.** These records explain *why* a thing is as it is. They are
-not a description of the system, and needing to read them to understand it would mean the design
-documents had failed.
-
-## Status, and views over it
-
-**Every document carries its state in YAML frontmatter** — research overviews, design documents,
-decision records, issue reports.
-
-> **Progressive insight — 2026-10-10.** For issue reports this stopped holding the day slice 3 of
-> [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)
-> landed: an issue's status, owner and fix live in the issue tracker, a module of the mesh, which refuses
-> a wrong status at the write; a report here carries the day it was opened and nothing of its status, and
-> `cycle.py` refuses a status key in a report opened after the import. The reports written before keep
-> their keys as a dated snapshot. The reason stands exactly as written — one place, never two — and the
-> one place for an issue is now the tracker, because a file every branch may edit was the place where
-> numbers collided and statuses were hard to see. Research overviews, design documents and decision
-> records carry their state here as before.
-
-> **Progressive insight — 2026-10-10, later.** The reports written before no longer keep their keys: once
-> the import was checked against every folder, one pull request removed them (ADR 0275's last note), and
-> `cycle.py` refuses a status key in any report.
-
-**There are no central status files.** Every cross-cutting view — a status matrix, a decision
-index, an open-issue list — is generated from frontmatter when asked for, never written to disk.
-Two places holding one fact drift, and the written one wins by being closer to hand.
-
-**Prose does not restate status.** One place, and two is one too many.
-
-## Workflows are playbooks
-
-Every workflow is a playbook in [`00-META/process/`](../00-META/process/): trigger, who runs it,
-steps, outputs. People and agents follow the same ones, and **agents do not act outside them**.
-
-Each is wrapped by a thin skill that defers to the playbook as authoritative and adds only the
-mechanical scaffolding — so the process has one definition rather than a document and an
-implementation that disagree.
-
-## Consequences
-
-- **A reader has one place per thing.** The design layer describes the system; these records say
- why; the playbooks say how work is done.
-- **Records will accumulate more slowly**, because the bar is a fork rather than a finding. This
- record is itself the correction: ten records became one because they were one decision.
-- **The public rule constrains everything written here**, permanently and at every commit. It is
- the reason research describes real observations without identifying the mesh it observed.
diff --git a/02-DECISIONS/0020-the-mesh-is-governed-by-a-constitution.md b/02-DECISIONS/0020-the-mesh-is-governed-by-a-constitution.md
deleted file mode 100644
index c43cbddb..00000000
--- a/02-DECISIONS/0020-the-mesh-is-governed-by-a-constitution.md
+++ /dev/null
@@ -1,64 +0,0 @@
----
-topic: how we work
-status: accepted
-date: 2026-07-10
-deciders: jochen
-reconstructed: true
----
-
-# 20. The mesh is governed by a constitution, injected where work is decided
-
-> Reconstructed after the fact from the evidence cited below.
-
-## Context
-
-By mid-2026 the mesh was doing a large share of its own design and implementation work through
-agents. The rules those agents were expected to follow existed — in operating instructions, in
-convention documents, in the knowledge base — but they were **retrieved**: an agent had to know
-a rule existed in order to look it up.
-
-Rules that must be looked up are followed by whoever already knows them, which is precisely the
-population that does not need them. The rules being violated were the ones nobody thought to
-search for.
-
-## Considered options
-
-1. **Documentation plus review.** Rejected — it is what existed. Review catches a violation
- after the work is done, and only if the reviewer knows the rule.
-2. **Lint and automated checks only.** Rejected as insufficient, not wrong. A check catches
- what can be expressed mechanically; most of these rules are about judgement — what belongs
- in a repository, when a criterion counts as verified.
-3. **A canonical rule set, injected into context wherever work is decided, with a check phase
- before output is accepted.** Chosen.
-
-## Decision
-
-A single canonical document states the mesh's non-negotiable rules. It is **injected
-proactively** into every eligible design and analysis session — agents do not fetch it, it
-arrives — and a check phase verifies the session's output against it before the work proceeds.
-
-It is a governed document, not a page. Changing it requires a proposal, sign-off by reviewers
-who are not the proposer, and a recorded decision. Drive-by edits are reverted.
-
-Scoped override pages may **tighten** it for a team or product. They may never relax it.
-
-## Consequences
-
-- A rule reaches the work whether or not anyone remembered it existed.
-- The check phase makes a violation a blocking outcome rather than a review comment.
-- Two copies of the same rules now exist: this document, and the reasoning in HQ that earned
- them. The enforced copy wins by default, so the reasoned copy quietly stops being true —
- which is why [`00-META/how-we-build.md`](../00-META/how-we-build.md) is now the source
- and the governed page is derived from it, via playbook
- [`05-constitution-sync.md`](../00-META/process/05-constitution-sync.md).
-- Injection costs context on every eligible turn, and grows with the document. Nothing
- currently bounds that.
-- The amendment process requires two reviewers, which a mesh with one human operator satisfies
- only by counting agents. That tension is real and unresolved.
-
-## References
-
-- The governed page was authored 2026-07-10 and carries its own amendment process.
-- `feat(noxflow): HAL architectural conformance gate for reviewer + architect` (#296),
- 2026-06-10 — the check phase, predating the document it checks against.
-- Knowledge base: `platform/constitution`.
diff --git a/02-DECISIONS/0021-hq-is-the-source-of-the-constitution.md b/02-DECISIONS/0021-hq-is-the-source-of-the-constitution.md
deleted file mode 100644
index 9317c7a8..00000000
--- a/02-DECISIONS/0021-hq-is-the-source-of-the-constitution.md
+++ /dev/null
@@ -1,68 +0,0 @@
----
-topic: how we work
-status: accepted
-date: 2026-08-23
-deciders: jochen
-reconstructed: false
----
-
-# 21. HQ is the source of the mesh constitution
-
-## Context
-
-[ADR 0020](0020-the-mesh-is-governed-by-a-constitution.md) established a canonical rule set,
-injected into every eligible design session and checked before output is accepted. It lives in
-the knowledge base, where the orchestrator reads it.
-
-HQ separately carried a document stating overlapping rules with the reasoning that earned each
-one. Two texts, one enforced and one not.
-
-That arrangement has a predictable outcome and it is not a tie. The enforced copy wins by
-default, because it is the one that blocks work. The reasoned copy quietly stops being true,
-and the rules survive without the incidents that justify them — at which point a rule reads as
-arbitrary, and an arbitrary rule is the kind people route around.
-
-## Considered options
-
-1. **The knowledge-base page is the source; HQ points at it.** Rejected, though it is the
- honest description of what was already happening. It leaves the reasoning downstream of the
- rule, and the reasoning is the part that makes a rule survive a challenge.
-2. **Accept the overlap and let both stand.** Rejected: two authorities is no authority, and
- the drift is silent.
-3. **HQ is the source; the governed page is derived and published from it.** Chosen.
-
-## Decision
-
-[`00-META/how-we-build.md`](../00-META/how-we-build.md) is the source. The governed page the
-mesh injects is **derived** from it — the rules without the reasoning — and is never edited
-directly.
-
-Publishing is a playbook step, not a manual act, and it ends with **reading the page back and
-verifying the change is present**. A publish that reported success and did nothing is exactly
-the failure class this mesh keeps producing
-([ADR 0010](0010-delivery.md)).
-
-Section numbering is stable, because the orchestrator and the review fragments cite sections by
-number.
-
-## Consequences
-
-- One source, many surfaces — the same argument HQ's separation already rests on
- ([ADR 0019](0019-how-this-repository-works.md)), applied to the rules themselves.
-- Each rule keeps the incident that earned it, in a place that is reviewed as a diff.
-- An edit to the derived page survives until the next sync and then vanishes. The playbook says
- so, and nothing mechanically prevents it.
-- **The sync is manual and is the weak point.** An unsynced rule is a rule the mesh does not
- enforce, whatever the source says — so the playbook requires the failure to be stated rather
- than passed over. This is the same class of gap as
- [`04-ISSUES/006`](../04-ISSUES/006-hq-is-not-indexed-into-the-knowledge-base/00-report.md),
- and it is worth watching for the same reason.
-- The document grew from four rules to seven sections, because it now has to carry everything
- the mesh enforces rather than only what someone thought to write down.
-
-## References
-
-- [`00-META/process/05-constitution-sync.md`](../00-META/process/05-constitution-sync.md) —
- the sync, including the read-back.
-- [ADR 0020](0020-the-mesh-is-governed-by-a-constitution.md) — the governed page and why it
- exists.
diff --git a/02-DECISIONS/0022-the-constitution-absorbs-what-is-enforced.md b/02-DECISIONS/0022-the-constitution-absorbs-what-is-enforced.md
deleted file mode 100644
index 7b7a6089..00000000
--- a/02-DECISIONS/0022-the-constitution-absorbs-what-is-enforced.md
+++ /dev/null
@@ -1,105 +0,0 @@
----
-topic: how we work
-status: accepted
-date: 2026-08-26
-deciders: jochen
-reconstructed: false
----
-
-# 22. The constitution absorbs what is already enforced
-
-## Context
-
-[ADR 0021](0021-hq-is-the-source-of-the-constitution.md) makes this repository the source
-and the knowledge base a derived copy, and playbook
-[05](../00-META/process/05-constitution-sync.md) publishes the copy whenever a rule changes.
-
-Three rules were accepted on 2026-08-25 and the sync came due. Reading the target before
-overwriting it — as [§2](../00-META/how-we-build.md) requires — found the two documents had
-diverged in a way nobody had recorded, and that a literal republish would have **deleted rules
-the mesh currently enforces**.
-
-| | source (`how-we-build.md`) | enforced page |
-|---|---|---|
-| §4 | naming and boundaries | **code quality** — structure, layering, types, restraint |
-| §5 | four evidence rules | one runtime-evidence rule |
-| §6 | review by a non-proposer | a design meeting with **two node operators** |
-
-The enforced page was last written 2026-07-10, is agent-authored, and its code-quality section
-appears in no decision record anywhere. It has been checked against for six weeks regardless.
-
-Two facts made this urgent rather than tidy:
-
-**Removal is not the safe option.** The orchestrator reads *"when absent, no constitution is
-injected (backward-compatible)"* — so deleting the page would not fail, it would silently
-inject nothing, and every design meeting would run unchecked with no error anywhere. Three
-unenforced rules would become all of them.
-
-**The stricter review bar has never been met.** The enforced page requires two node operators,
-neither the proposer. There is one operator. Every amendment ever made violated it.
-
-## Considered options
-
-1. **Republish wholesale.** What the playbook literally says. Rejected — it deletes the
- code-quality rules, which exist nowhere else.
-2. **Merge surgically** — update §2, §3, §5 in the copy and leave §4 alone. Nothing is lost and
- it is quick, but the source becomes authoritative for some sections and the copy for others,
- which is the drift being fixed.
-3. **Absorb, then republish.** Chosen.
-
-## Decision
-
-**What the mesh enforces and cannot cite is written down here first, then republished.**
-
-The code-quality rules become §8 rather than §4, because appending renumbers nothing and every
-existing citation stays valid.
-
-**They are recorded as inherited, and marked as such.** Every other rule in the document states
-the incident or measurement that earned it. These state nothing, because nothing was ever
-written down. Importing them silently would have made the document claim a provenance it does
-not have, and this repository's whole argument is that the reasoning is the expensive half.
-
-**The review bar is resolved in favour of what is achievable.** A rule requiring two operators
-where there is one is not a high standard, it is a rule everything silently violates — the same
-shape as a firewall key declared in five manifests and read by no code
-([04-ISSUES/003](../04-ISSUES/003-firewall-scope-is-read-by-no-code/00-report.md)). Review means
-a person who is not the proposer, which is both achievable and what has been practised.
-
-## Consequences
-
-- **The copy can now be a faithful projection**, which is what makes "the source is the source"
- true rather than aspirational.
-- **§8 has no reasoning behind it and says so.** Anyone may propose replacing an inherited rule
- with an earned one; until then the marking is the honest state. It is also a standing
- invitation to delete any of them that turn out to serve nothing.
-- **Six weeks of drift went unnoticed** because nothing compares the two. The sync is manual and
- runs only when someone remembers a rule changed. **Nothing detects divergence**, and this
- record does not fix that — it is worth an issue.
-- **The operator's instinct to remove the copy was right about the direction and wrong about the
- method.** The end state where the mesh reads this repository directly — rendered and operable
- through the board — removes the second copy rather than blanking it. That is not designed and
- is not decided here.
-- **Section numbering is now append-only in practice.** §8 sits after the overrides section
- purely because renumbering would break citations, which is a cost of the copy existing at all.
-
-## Sync
-
-Run 2026-08-26, and the read-back earned its place in the playbook.
-
-**The first publish reported success and changed nothing.** It created a new revision and
-updated the title, and the body did not apply — a malformed argument was dropped silently. Had
-the sync been marked done on the strength of the tool saying *"updated; new revision created"*,
-three accepted rules would have gone unenforced while every record said otherwise, and nothing
-would ever have contradicted it.
-
-That is §5 demonstrating itself during its own publication: a green result proves transport,
-not effect. The republish was verified by reading the rule back out of the live page, not by
-trusting the second success message either.
-
-## References
-
-- [ADR 0021](0021-hq-is-the-source-of-the-constitution.md) — source and copy.
-- [ADR 0020](0020-the-mesh-is-governed-by-a-constitution.md) — why the copy is injected at all.
-- [Playbook 05](../00-META/process/05-constitution-sync.md) — the sync this record interrupts.
-- [ADR 0017](0017-a-test-defends-a-decision.md), [ADR 0018](0018-a-picture-is-read-from-what-runs.md),
- [ADR 0012](0012-the-mesh-creates-no-symlinks.md) — the three rules whose sync surfaced this.
diff --git a/02-DECISIONS/0023-approval-is-the-checkpoint.md b/02-DECISIONS/0023-approval-is-the-checkpoint.md
deleted file mode 100644
index 2ec3fadc..00000000
--- a/02-DECISIONS/0023-approval-is-the-checkpoint.md
+++ /dev/null
@@ -1,78 +0,0 @@
----
-topic: how we work
-status: accepted
-date: 2026-08-26
-deciders: jochen
-reconstructed: false
----
-
-# 23. The approval is the checkpoint, not the second pair of hands
-
-## Context
-
-[§2](../00-META/how-we-build.md) reads *"one change per pull request, and never merge your
-own"*, and gives the reason: *"self-merging removes the checkpoint that is the entire point."*
-
-The rule was written for people. Applied literally to an agent it produces a contradiction
-that surfaced immediately: an agent asked to merge cannot merge, because it authored what it
-is being asked to merge. Every merge this repository has seen was therefore either performed
-by the thing that wrote it, or not performed at all.
-
-Stated by the operator: *"you are allowed to self-merge, after I've given you explicit
-approval."*
-
-## Considered options
-
-1. **Read it literally.** An agent never merges; the operator merges by hand every time.
- Rejected: it makes the operator a mechanical step rather than a reviewer, and a step
- performed dozens of times without reading is not a checkpoint — it is the *shape* of one,
- which is worse, because the record then claims a review that did not happen.
-2. **Drop the rule for agents.** Rejected: the rule is right, and the failure it prevents —
- work merged with nobody having looked — is not one an agent is less prone to.
-3. **Require notification and approval, and stop there.** Chosen. Who performs the merge is
- not the thing worth constraining.
-
-## Decision
-
-**Every merge into the main branch is notified and approved.** Stated by the operator in
-exactly those terms, and the whole of the rule.
-
-Notified: the merge is proposed and said out loud, not performed and mentioned. Approved: a
-person says yes to *that merge*. Who then performs it does not matter, which is what makes an
-agent merging its own work unremarkable — the checkpoint already happened.
-
-**What approval is not**, because this is the half that can rot:
-
-- A standing permission granted once and cited forever.
-- An instruction to do the work, read as approval to merge it.
-- Silence.
-- The author's own judgement that the work is ready.
-
-## Consequences
-
-- **The record must show which it was.** A merge performed on approval and a merge performed
- unilaterally look identical afterwards, and the difference is the whole rule. Until the forge
- can record an approval, the merge commit says so — which is weaker than a recorded review and
- is stated here rather than left to be discovered.
-- **Branch protection would make this structural instead of textual.** Required approvals turn
- the rule into something the forge enforces. It cannot be set through the API on the forge
- version in use, so this remains a rule held by intention — the condition
- [§5](../00-META/how-we-build.md) exists to name.
-- **The one-change-per-pull-request half is untouched**, and was broken twice on 2026-08-26.
- Both breaches are recorded in the pull requests themselves rather than in a conversation.
-- **This narrows a rule rather than relaxing one.** The obligation moves from *who performs the
- merge* to *whether a person decided*, which is a higher bar in the case the original wording
- actually permits: a reviewer who merges someone else's work without reading it satisfies the
- old rule and fails this one.
-
-## Sync
-
-Run and verified 2026-08-26. §2 in the enforced page now reads *never merge unapproved work*,
-confirmed by reading the rule back out of the live page rather than by trusting the publish —
-the previous sync reported success and changed nothing.
-
-## References
-
-- [`how-we-build.md`](../00-META/how-we-build.md) §2 — the rule, now carrying this.
-- [ADR 0010](0010-delivery.md) — the standard a checkpoint is held to: a
- step that reports success without doing anything is the fault, not the shortcut.
diff --git a/02-DECISIONS/0024-model-access-is-a-provision.md b/02-DECISIONS/0024-model-access-is-a-provision.md
deleted file mode 100644
index c40bbb74..00000000
--- a/02-DECISIONS/0024-model-access-is-a-provision.md
+++ /dev/null
@@ -1,95 +0,0 @@
----
-topic: what runs on it
-status: accepted
-date: 2026-08-30
-deciders: jochen
-reconstructed: false
----
-
-# 24. Model access is a provision, and a licence is a thing with a name
-
-## Context
-
-Everything in this mesh that thinks needs a model, and there is more than one way to reach one:
-
-| | |
-|---|---|
-| **hosted services** | several vendors, each with its own account, quota and key |
-| **models the mesh runs itself** | open-weight models on a node with the hardware for them |
-
-And the choice is **per consumer, deliberately**: a workstation's own session on one account, a
-laptop on the organisation's, two hired workers on the mesh's local model because their work does
-not justify paid tokens. Those are three different answers to one requirement, held at once, in
-one mesh.
-
-**The existing system has the hard half of this already** — automatic licence refresh and
-switching between accounts when one is exhausted — and it works. It is not being replaced because
-it was wrong; it is being rebuilt because it lives in a place that cannot express the rest.
-
-## Decision
-
-**Model access is a provision.** A module that needs to think declares `requires: model-access`;
-anthropic, openai, grok and a locally-run model are four modules that provide it. That is
-[ADR 0009](0009-modules-and-the-graph.md)'s mechanism unchanged, and it buys the things that
-mechanism already buys: several implementations of one job, a refusal when more than one could
-answer, and choosing by assigning the one you want.
-
-**A locally-run model needs nothing new at all.** It is a mesh-scoped provision on the node with
-the hardware — the same shape as a database, including the credential.
-
-### A licence is a named thing, and the name is the operator's
-
-Not an anonymous credential hanging off a provider. *The personal account*, *the organisation's
-account* — those are names a person uses, and the mesh has to use them too, because the whole
-point is saying **which one** a given consumer uses.
-
-**Many to many.** One provider has several licences; one licence serves several consumers. So it
-is **not a claim** — claims are for things only one holder may have, and two machines sharing an
-account is the ordinary case rather than a collision.
-
-### Four things this needs that the mesh does not have
-
-Written as gaps rather than as design, because each is a real piece of work and pretending
-otherwise is how a plan becomes a surprise.
-
-**1. A provider that is not on a node.** A mesh-scoped provision today is answered by *the machine
-running it*, and the reachability rule refuses two ends that share no private network. A hosted
-service is on nobody's machine and is reached over the public internet. That is a third scope —
-answered by a record rather than by a node — and the reachability rule must not apply to it.
-
-**2. A secret the mesh is given rather than one it mints.** Every credential the mesh handles
-today it generated itself, sealed to both ends, and discarded. An API key arrives from a person.
-The missing verb is *accept*: take a value, seal it to each holder, and **discard the plaintext**
-— because a mesh that keeps operator-supplied keys readably is the arrangement this project
-[measured and rejected](0009-modules-and-the-graph.md).
-
-**3. A consumer that is not a machine.** *This worker uses that licence* is a binding to an agent,
-not to a node. [ADR 0001](0001-mesh-brokers-nodes-host-agents-think.md) already says an agent
-holds credentials and that delivery follows its node bindings and modality — so what is delivered
-still lands on a machine, and what is **chosen** is chosen per agent. The provisions model has no
-consumer identity other than a node.
-
-**4. Switching is a reaction, not a declaration.** Everything here is desired state, reconciled by
-comparison. A licence that hits its limit and must be swapped is a response to something observed,
-and it cannot be expressed as a declaration without the declaration meaning *whatever is working
-right now* — which is not a thing anybody declared. **It belongs with observability, changing a
-binding**, and the binding is then declared as usual. Saying this plainly is what stops the
-declaration language growing a conditional.
-
-## Consequences
-
-- **The refusing rule applies here and will be felt.** A mesh holding three ways to reach a model
- refuses every consumer that has not said which — which is correct and is a great deal of
- saying-which the first time. The remedy is one assignment per consumer, and the message names
- the candidates.
-- **A licence outliving its holder is a live credential nobody is watching.** The same rule the
- provisioner follows applies: what the mesh granted and no longer grants is withdrawn.
-- **Nothing here makes a node authenticate to a model provider.** ADR 0001 holds: an agent does.
- What changes is that the mesh can now say *which agent, which licence, which node it lands on*,
- which is the fact ADR 0001 records as missing.
-
-## References
-
-- [ADR 0009](0009-modules-and-the-graph.md) — provisions, scope, choosing, and sealed credentials
-- [ADR 0001](0001-mesh-brokers-nodes-host-agents-think.md) — agents hold credentials, not nodes;
- `hal/ai` as the context owning provider grants and rotation
diff --git a/02-DECISIONS/0025-the-design-record-is-read-not-copied.md b/02-DECISIONS/0025-the-design-record-is-read-not-copied.md
deleted file mode 100644
index 01972371..00000000
--- a/02-DECISIONS/0025-the-design-record-is-read-not-copied.md
+++ /dev/null
@@ -1,111 +0,0 @@
----
-topic: how we work
-status: accepted
-date: 2026-08-31
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0019-how-this-repository-works.md
----
-
-# 25. The design record is read where it is written, never copied to be found
-
-## Context
-
-**These documents cannot be found by searching the mesh's memory, and never could.** Checked on
-2026-08-23 and again on 2026-08-31, against both the symptom-indexed store and the structured
-archive, using a decision record's full title and a distinctive phrase from a design document: no
-result, no partial match, no stale copy.
-
-That matters because of what was promised. The objection to giving this material its own
-repository was that the mesh already has a knowledge store, and a second one repeats the mistake
-that store was created to fix. **The answer offered was indexing rather than location** — that
-these documents would be returned beside everything else in a search, so where they were authored
-became a separate question. The indexing was never built.
-
-**The claim has since stopped being load-bearing**, which is why this is a decision rather than an
-incident. [`README.md`](../README.md) names the gap in the place the claim used to sit, and
-[ADR 0019](0019-how-this-repository-works.md)'s reasoning rests on cadence, reviewers and scope —
-none of which depend on being searchable from elsewhere. What remained was an unbuilt capability
-and an open question, recorded as
-[`04-ISSUES/006`](../04-ISSUES/006-hq-is-not-indexed-into-the-knowledge-base/00-report.md).
-
-**A signpost was added on 2026-08-31 and measured.** One entry in the mesh's memory naming what
-lives here and when to come looking. A search for *design records, decisions, repository* returns
-it; a search phrased the way somebody actually asks — *why is the mesh built this way* — returns
-nothing, because the store matches terms and not meaning. **Reachable is not the same as
-surfacing**, and the measurement is what established which one a signpost buys.
-
-## Considered Options
-
-1. **A one-way sync into the mesh's memory.** A job reads this repository on a schedule and writes
- the documents into the searchable store. It works with what exists today and needs nothing
- built first. **Rejected**, because it creates a second copy of every document, and the failure
- mode of a derived copy is the one this repository is least able to tolerate: *the copy that is
- searched quietly stops matching the copy that is edited*, and the enforced one wins. A design
- record that has silently diverged from the reasoning it claims to carry is worse than one that
- cannot be found — the first misleads, the second merely fails.
-
-2. **Leave the signpost and close nothing.** Honest, free, and it keeps the gap visible.
- **Rejected as an end state**, though it is what stands until the option below exists. It
- answers only for a reader who already suspects these documents exist, which is precisely not
- the reader the mesh's memory is designed for.
-
-3. **An agent reads this repository directly, and the search consults it.** Nothing is copied.
- **Adopted.**
-
-## Decision
-
-**The design record is read where it is written.** Retrieval is an agent reading this repository,
-not a copy living in a second store — and a search of the mesh's memory consults that agent, so
-what it knows appears beside ordinary results rather than only when it is asked.
-
-Both halves are the decision. The first alone is merely a reader, and would leave this repository
-reachable but not surfacing — the state measured above. **The second half is what discharges the
-promise** that these documents are returned beside everything else.
-
-**There is no copy, and that is the point.** No sync, no schedule, no reconciliation, and nothing
-that can drift, because there is only ever one of each document. It is also always current,
-including for work that is not yet committed.
-
-**The direction of reading is one-way and stays that way.** The agent reads this repository and
-answers from it. Nothing flows back: this repository is public, the mesh is not, and a return path
-would be how installation-specific detail arrives into documents that must not carry it
-([`README.md`](../README.md)).
-
-> **The mechanism changed — 2026-09-30, by ADR 0153.** What stands: read where it is written, no copy,
-> one-way. What moved: the reader is a module the mesh assigns (`records`, a checkout at a commit every
-> answer names) rather than the agent session of design 15, which is not built; and "the search consults
-> the agent" has no store to consult since the cut-over — the console's tool list is where the record
-> appears beside everything else. [ADR 0153](0153-the-record-is-read-by-a-module-and-the-console-lists-it.md).
-
-## Consequences
-
-**This repository stops being a fourth knowledge system, properly.** The original objection was
-about adding a knowledge *system*. An agent with read access adds no store at all — which answers
-the objection more completely than the indexing that was promised, rather than merely as well.
-
-**ADR 0019's promise is amended, not satisfied.** It said these documents would be *indexed*. They
-will not be. They will be *read*, and the search will ask. The commitment that survives is the one
-that mattered — that a searcher finds them without already suspecting they exist — and the
-mechanism behind it is different from the one named.
-
-**It is gated on an agent that does not exist yet.** Until it does, the signpost is what stands,
-and this repository is reachable rather than surfacing. That is a known and stated gap, not a
-silent one — and the gap is now a build task with a decided shape rather than an open question.
-
-**The search must degrade honestly.** When the agent cannot be reached, a search has to say that
-this material was not consulted. A result set that silently omits it looks identical to one where
-nothing matched, and *silence and success must never look alike*
-([ADR 0004](0004-a-node-and-how-it-joins.md)) — the rule this repository has now paid for twice.
-
-**A rule states how it is checked, and this one is checkable.** The check is the measurement that
-produced this record: search the mesh's memory for a phrase that appears only in a design document
-here, and require it back. That check fails today, deliberately, and passing it is what closes
-`04-ISSUES/006`.
-
-## References
-
-- [`04-ISSUES/006`](../04-ISSUES/006-hq-is-not-indexed-into-the-knowledge-base/00-report.md) —
- the gap, the two measurements, and why closing it early was refused
-- [ADR 0019](0019-how-this-repository-works.md) — the promise this amends
-- [`README.md`](../README.md) — the objection, and the gap named where the claim used to sit
diff --git a/02-DECISIONS/0026-the-mesh-has-a-session-of-its-own.md b/02-DECISIONS/0026-the-mesh-has-a-session-of-its-own.md
deleted file mode 100644
index 7fbfb1d1..00000000
--- a/02-DECISIONS/0026-the-mesh-has-a-session-of-its-own.md
+++ /dev/null
@@ -1,130 +0,0 @@
----
-topic: what runs on it
-status: accepted
-date: 2026-08-31
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0004-a-node-and-how-it-joins.md
----
-
-# 26. The mesh has a session of its own, and it is the node session's mechanism
-
-## Context
-
-[ADR 0004](0004-a-node-and-how-it-joins.md) gives every node a session: one per node, permanent,
-remembering across callers, its system prompt the node's engram, reachable over the broker like
-everything else. **Any node can message any node**, and that is called the one part of the system
-that is genuinely a mesh — symmetric, with no centre.
-
-**There is no way to address the mesh itself.** A question that spans machines — *what is running
-across all of this*, *which nodes are behind*, *why is it built this way* — has to be put to some
-node, which then asks the others. That works, and it makes a mesh-wide question **nobody's
-question**: every node answers it as a foreigner, from a position where the whole is not in view.
-
-**Three things independently arrived at the same missing piece.**
-
-[ADR 0025](0025-the-design-record-is-read-not-copied.md), taken hours before this one, commits to
-an agent that reads the design repository directly and answers into search. That agent has to
-exist, run somewhere, and be askable — and nothing in the record says what it is or where it
-lives.
-
-[`14-model-access.md`](../03-DESIGN/01-to-be/14-model-access.md) records, as a gap deliberately
-not half-built: *this worker uses that licence is a binding to an agent, not to a node* — and the
-provisions model has no consumer identity other than a node. A session that must be assigned a
-licence is exactly that consumer, and node sessions are already one.
-
-**And ADR 0004 never said how a session is set up.** It describes behaviour and stops: nothing
-states how a session starts, where its context lives, how the engram reaches it, or how a message
-off the broker becomes a prompt. There is no design document for it. That gap was invisible until
-something had to be built *like* a node session, because describing a second instance of a
-mechanism requires the mechanism to have been described once.
-
-## Considered Options
-
-1. **No mesh session; keep relaying through a node.** Costs nothing and works today. **Rejected.**
- It leaves mesh-wide questions belonging to nobody, and it does not survive contact with
- ADR 0025 — that agent still needs a home, so the thing gets built anyway, unnamed, as an
- attachment to whichever node happened to host it.
-
-2. **A new kind of agent, built separately.** Purpose-built for the whole mesh. **Rejected.** It
- would hold a session, a memory, a licence and broker plumbing — every one of which the node
- session already has. Two implementations of one mechanism drift, and the vocabulary collision
- that [ADR 0001](0001-mesh-brokers-nodes-host-agents-think.md) exists to undo began exactly this
- way: two things that were nearly the same, built twice, until neither word meant one thing.
-
-3. **The same mechanism, started in a different context.** **Adopted.**
-
-## Decision
-
-**The mesh has one session, addressed as the mesh, and it is a node session in every respect but
-three.**
-
-| | |
-|---|---|
-| **the context it starts in** | the mesh's, not a machine's — this is the whole of what makes it different |
-| **its engram** | the mesh's system prompt, as a node's engram is that node's |
-| **its licence binding** | assigned in its own right, not inherited from the machine it runs on |
-
-Everything else is unchanged and deliberately so: it is permanent, it remembers, it is reachable
-over the broker, it holds its own tools, and switched off it still answers *I am switched off*
-rather than falling silent.
-
-**It runs on the node that holds the control plane** — not for convenience, but because that node
-is already the one place excepted from *compromise of a node is compromise of that node*
-(ADR 0004). An agent able to reach everything, placed anywhere else, creates a **second** such
-place. Putting it where the authority already sits concentrates nothing new.
-
-**It is an addition to per-node messaging and never a replacement.** Every node remains directly
-addressable. This is not a preference: ADR 0001 holds that losing the control plane costs *change,
-not operation*, and a mesh whose only conversational surface lives on that node would lose the
-ability to ask anything while every machine kept running perfectly. **The front door may not be
-the single point.**
-
-**It is not an employee** ([ADR 0003](0003-agents-are-persistent-employees.md)). Nobody hires it,
-it holds no task queue, it is never drained or reassigned. What it does with work that belongs
-somewhere else is **dispatch it** — to node sessions, or to workers — which is what a node session
-already does when asked something it does not have.
-
-**It is ADR 0025's reader.** The agent that reads the design repository and answers into search is
-this session, not a second one. One agent, one memory, one place to reach; two would both need
-that repository and would eventually disagree about what it says.
-
-**"One per node" is about address, not about process count.** ADR 0004's rule — *two and nothing
-decides which replies* — forbids ambiguity in who answers when a **node** is addressed. The mesh
-session answers when the **mesh** is addressed. The control-plane node therefore hosts two
-sessions and no ambiguity, and stating this here is what stops it reading as a contradiction
-later.
-
-## Consequences
-
-**The node session's setup must now be designed, and it never was.** This decision is expressed as
-*the same as a node session, elsewhere*, which is only meaningful once that mechanism is written
-down. The design document covering both is the immediate consequence of this record, not a
-follow-up to it.
-
-**A consumer that is not a machine stops being deferrable.** The licence binding above is the gap
-`14-model-access.md` names, and it now has two consumers rather than a hypothetical one. Until it
-exists, a session's model access can only be expressed as *this module on this machine*, which
-cannot say *this node's session uses the personal licence and the mesh's uses the company one* —
-the thing the binding is for.
-
-**Symmetry is preserved, and it is worth being precise about why.** ADR 0004's claim is about what
-a node can reach, and it is untouched: node-to-node messaging is unchanged, nothing is routed
-through the mesh session, and it is a participant rather than a hop. What arrives is a
-participant that happens to be the one a person usually addresses.
-
-**Availability degrades to inconvenience rather than to silence** — but only because of the
-addition rule above. If that rule is ever relaxed, this consequence inverts, and it inverts
-quietly: everything keeps working and nobody can ask about it.
-
-**The surface a person uses is not decided here.** That a board is a good place to talk to it is
-likely and is not this record's business; the session is reachable over the broker like everything
-else, and what puts a text box in front of it is a separate choice.
-
-## References
-
-- [ADR 0004](0004-a-node-and-how-it-joins.md) — the node session this extends
-- [ADR 0025](0025-the-design-record-is-read-not-copied.md) — the reader this session is
-- [ADR 0003](0003-agents-are-persistent-employees.md) — the vocabulary this is not
-- [`03-DESIGN/01-to-be/14-model-access.md`](../03-DESIGN/01-to-be/14-model-access.md) — *a
- consumer that is not a machine*, the gap this makes concrete
diff --git a/02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md b/02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md
deleted file mode 100644
index 61c9d50f..00000000
--- a/02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md
+++ /dev/null
@@ -1,109 +0,0 @@
----
-topic: what runs on it
-status: accepted
-date: 2026-08-31
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0009-modules-and-the-graph.md
----
-
-# 27. A provision names what the consumer is coupled to, not the role it plays
-
-## Context
-
-Provisions are named after roles. The catalogue and every test fixture built so far say:
-
-```
-provides: database
-requires: database
-```
-
-**Nothing distinguishes one engine from another.** A module requiring `database` is satisfied by
-any module providing `database`, so a module written against PostgreSQL can be matched to a
-provider of Microsoft SQL Server, resolve as satisfied, deploy, and fail on its first query.
-
-**The mesh runs several engines** — PostgreSQL, Microsoft SQL Server, MariaDB, and others behind
-products that expose their own. This is not a hypothetical collision.
-
-**The failure is in the direction that hides.** Resolution *succeeds*. Nothing is refused, nothing
-is logged, and the breakage surfaces later as an error inside an application, on a machine, with
-nothing connecting it back to a match made elsewhere by something that thought it had done its
-job. **A wrong answer delivered confidently costs more than a refusal**, and the whole point of
-refusing on ambiguity ([ADR 0009](0009-modules-and-the-graph.md)) was to not do this.
-
-**How it got in:** every test written for the resolver had exactly one provider of each name, so no
-mismatch was expressible and none was caught. The fixtures agreed with the design. That is the same
-fault as [`04-ISSUES/005`](../04-ISSUES/005-pipeline-test-harness-unbuildable/00-report.md)'s
-imagined output and [`019`](../04-ISSUES/019-a-comment-asserting-a-fact-about-a-machine/00-report.md)'s
-unchecked comment, at the level of a name rather than a line.
-
-## Considered Options
-
-1. **Keep role names; let the operator assign correctly.** The mesh would refuse ambiguity when two
- providers exist, so a person picks. **Rejected.** It makes correctness depend on somebody
- knowing that the module they are assigning speaks a particular dialect — which is exactly the
- knowledge the provisioning model exists to remove. And with one provider of each name, nothing
- is ambiguous and nothing is asked.
-
-2. **A role name plus a `flavour:` or `engine:` qualifier**, matched as a second field.
- **Rejected.** Two fields that must agree is a constraint the resolver has to enforce and a
- manifest author has to remember, to express something one field already can. The name is the
- contract; splitting it invites a requirement that names a role and forgets the qualifier, which
- then matches everything again.
-
-3. **The name says what the consumer is coupled to.** **Adopted.**
-
-## Decision
-
-**A provision is named for the thing a consumer's code is written against.**
-
-```
-provides: postgres-database
-requires: postgres-database
-```
-
-**The test is whether the consumer can tell the difference.** If swapping the provider would break
-the consumer, the name must say which provider — because a match that breaks the consumer is not a
-match. If the consumer genuinely cannot tell, a role name is correct and better.
-
-| provision | | why |
-|---|---|---|
-| `postgres-database`, `mssql-database` | **specific** | applications are written against a dialect; a swap breaks them |
-| `route` | **role** | the consumer wants its name reachable and does not care what proxies it |
-| `resolver` | **role** | the consumer wants names to resolve |
-| `artifact-store` | **role** | the consumer fetches by digest over a protocol, and nothing else |
-
-**`database` is not a provision and may not be provided.** There is no context in which an
-application talks to a generic database: it talks to PostgreSQL or it talks to SQL Server. A name
-that cannot be true of any real consumer should not be expressible.
-
-**This is about coupling, not about products.** Two providers of `postgres-database` — a container
-on this node and a managed instance elsewhere — are interchangeable and *should* both match. What
-may not be interchangeable is what the consumer's queries are written in.
-
-## Consequences
-
-**Every manifest that names a database changes.** Doing this now costs a rename across a handful of
-examples. Doing it after modules are migrated costs it across all of them, plus every deployment
-that resolved against the old name.
-
-**Wrong requirements now fail loudly, and at the right moment.** A module requiring
-`postgres-database` where only `mssql-database` is provided is unsatisfiable, so it is **refused at
-resolution** with both names visible — rather than deployed and broken later. This is the property
-that was lost, restored.
-
-**Generic role names are still right, and the rule says when.** This does not push specificity
-everywhere; it puts it exactly where a consumer is coupled. Naming `route` after a particular proxy
-would be the same error in the other direction, and would prevent a swap that genuinely changes
-nothing.
-
-**It is checked, not merely stated** ([`00-META/how-we-build.md`](../00-META/how-we-build.md) §5).
-A manifest providing a name known to be engine-generic is refused, naming what to say instead.
-Without that, this record is a convention, and a convention is what the previous naming was.
-
-## References
-
-- [ADR 0009](0009-modules-and-the-graph.md) — provisions, and refusing on ambiguity
-- [`03-DESIGN/01-to-be/07-the-foundation.md`](../03-DESIGN/01-to-be/07-the-foundation.md) — *the
- provisioning model uses databases, roles and schemas as PostgreSQL means them*, which is this
- record's point made about the substrate before it was made about modules
diff --git a/02-DECISIONS/0028-the-substrate-supplies-the-control-plane-and-nothing-else.md b/02-DECISIONS/0028-the-substrate-supplies-the-control-plane-and-nothing-else.md
deleted file mode 100644
index 0b6bc066..00000000
--- a/02-DECISIONS/0028-the-substrate-supplies-the-control-plane-and-nothing-else.md
+++ /dev/null
@@ -1,118 +0,0 @@
----
-topic: the tiers
-status: accepted
-date: 2026-08-31
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0006-the-substrate-and-the-control-plane.md
----
-
-# 28. The substrate supplies the control plane and nothing else
-
-*Corrects one row of [ADR 0006](0006-the-substrate-and-the-control-plane.md) and makes explicit
-something it left unsaid. The rest of that record stands.*
-
-## Context
-
-ADR 0006 defines the substrate by a circularity: **what the control plane needs in order to run,
-and cannot ask itself for, because it is not running yet.** Two questions, and both must be
-answered *yes* for something to be substrate.
-
-Its membership table admits the object store on this line:
-
-| role | product | |
-|---|---|---|
-| object store | **MinIO** | it cannot grant itself a bucket |
-
-**That answers the second question and assumes the first.** It is true that a control plane cannot
-grant itself a bucket. Nothing establishes that it needs one.
-
-**It does not.** Verified 2026-08-31 against `mesh-control`: no S3 client, no bucket, no object
-storage of any kind outside comments. Artifacts reach nodes as content-addressed blobs in the OCI
-registry, and the code records the decision and its reasoning:
-
-> One store, and it is the registry the bootstrap already pulls from. An OCI registry is a
-> content-addressed blob store that happens to also understand images… The alternative considered
-> was a second store beside it — S3-shaped, buckets, signed URLs. It is the right answer for
-> objects that are *mutable*, or need per-reader access, or are not build output. None of that
-> describes a digest-pinned archive, and standing up a second service to hold one kind of
-> immutable blob means two things to run, two things to back up and two ways for an artifact to be
-> missing.
-
-**The row is inherited from the system being replaced**, where an object store distributed module
-tarballs. Here nothing does, and the row was never re-tested against the definition it sits under.
-
-**A second thing ADR 0006 never says:** whether a substrate service and a module of the same
-product are the same instance. It says the substrate is *not the control plane* and *not a place
-for logic*, and stops. The question is not idle — an application wanting a database, on a mesh
-whose substrate is already running PostgreSQL, has an obvious wrong answer available.
-
-## Considered Options
-
-1. **Leave the object store as substrate, unused.** Harmless-looking. **Rejected.** A membership
- list that includes something nothing needs is a list that has stopped being derived from its
- test, and the next member is admitted by precedent instead of argument. It also mandates that
- every mesh run a service no mesh uses.
-
-2. **Applications share the substrate's instances.** One PostgreSQL, one of everything.
- **Rejected**, below.
-
-3. **The substrate is exactly what the control plane consumes; everything else is a module.**
- **Adopted.**
-
-## Decision
-
-**The object store is not substrate.** It fails the first half of the test: the control plane does
-not need one. An object store is an ordinary module, required through the module graph like
-anything else, and a module wanting one depends on a module providing one.
-
-**The substrate has four members, not five**: a relational store, a message bus, an image registry,
-and conditionally an identity provider. The registry stays — the control plane genuinely cannot
-deliver an artifact without somewhere to put it.
-
-**A substrate service and a module of the same product are different instances, and are not
-shared.** The mesh's own PostgreSQL and a PostgreSQL a workload was given are two servers, two
-containers, two lifecycles.
-
-Three reasons, and the first is the one that matters:
-
-**The substrate is not in the module graph.** It is raised from the pinned bundle the host carries,
-before any mesh exists to declare it. A workload depending on it would depend on something the
-graph cannot see, cannot rotate a credential for, and cannot move — which is every property the
-provisioning model exists to provide.
-
-**It would put workload data in the control plane's own store.** The mesh's contexts own their
-stores exclusively ([ADR 0008](0008-a-context-owns-its-store.md)). An application sharing that
-server can exhaust it, lock it, or fill its disk, and the failure is the control plane going down
-— which is the one failure that makes every other one harder to fix.
-
-**They are bounded differently.** The substrate is sized, backed up and upgraded as part of
-bootstrapping a mesh. A workload's database follows the workload — moved with it, destroyed with
-it, restored with it.
-
-## Consequences
-
-**Migrating an object store is ordinary module work**, not substrate work. It was previously going
-to be done as part of completing the substrate, which would have been the wrong shape and would
-have coupled every mesh to a service the mesh does not use.
-
-**A mesh with no workload needing one runs no object store at all.** That is the correct outcome
-and was not previously available.
-
-**Two PostgreSQL containers on a node that hosts both is expected**, not duplication to be
-optimised away. Anyone tidying them together should find this record first.
-
-**"Substrate by role and ordinary by delivery" loses one of its two members.** ADR 0006 uses that
-phrase of the object store and the registry — things that are substrate but provisioned once a
-control plane exists. It now describes the registry alone.
-
-**The definition is applied, not just stated.** Both halves of the circularity test are asked of
-each member, and *cannot grant itself one* is not sufficient on its own — it is true of almost any
-service, which is what made it possible to admit a member on that half alone.
-
-## References
-
-- [ADR 0006](0006-the-substrate-and-the-control-plane.md) — the definition, and the table this
- corrects one row of
-- [ADR 0008](0008-a-context-owns-its-store.md) — a context owns its store exclusively
-- `mesh-control internal/builder/registry.go` — where artifacts go, and why not S3
diff --git a/02-DECISIONS/0029-a-network-is-a-shape-because-an-action-cannot-be-undone.md b/02-DECISIONS/0029-a-network-is-a-shape-because-an-action-cannot-be-undone.md
deleted file mode 100644
index 94acc9b9..00000000
--- a/02-DECISIONS/0029-a-network-is-a-shape-because-an-action-cannot-be-undone.md
+++ /dev/null
@@ -1,112 +0,0 @@
----
-topic: the tiers
-status: accepted
-date: 2026-08-31
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0005-the-node-host.md
----
-
-# 29. A network is a shape, because an action cannot be undone
-
-## Context
-
-**A module of several containers has no way to let them reach each other by name.** A container
-declaration carries a `network` field, and it only ever *joins* one that already exists — it was
-added so the control plane could reach the store and the broker on the machine it was raised on.
-Nothing in the vocabulary **creates** a network.
-
-Without one, containers on a machine share the runtime's default bridge, which gives addresses and
-no name resolution between them. So a module that is several containers can only be written by
-publishing ports onto the machine and pointing its own parts at the host — which puts a module's
-private wiring on the machine's own address space, where anything else on the machine can reach it
-and any other module can collide with it.
-
-**This is the gap a mail system meets and nothing else so far does**
-([`00-work-breakdown.md`](../03-DESIGN/01-to-be/00-work-breakdown.md) 3.3). It is being taken now
-rather than then, because 3.3 is the task most likely to send work back into the declaration
-language and the least useful place to discover it.
-
-**Adding a shape is not a small change, and the host says so** — the vocabulary is asserted
-against a stated number, with the reason written into the failure: *every addition widens what a
-compromised control plane can express, so a change here is a decision.* The host applies what it
-is told; the only bound on a hostile control plane is what the language can say
-([ADR 0004](0004-a-node-and-how-it-joins.md)).
-
-## Considered Options
-
-1. **An `action` that creates the network.** The vocabulary already has one, the bundle already
- uses seven of them, and `docker network create` with a `verify` is exactly the shape an action
- takes. Nothing would need adding. **Rejected**, on removal:
-
- > An action has no footprint the host can undo — it ran, and whatever it did belongs to
- > whatever it acted on.
-
- A network made this way **leaks when the module is unassigned**, and the mesh cannot tell: the
- record says an action ran, and there is nothing to reverse. Unassigning a module would leave a
- network behind on every machine it was ever on, and the only way to find them would be to go
- and look. *A resource the mesh can create and never clean up is one it should not create.*
-
- There is a second reason, and it is the one that generalises: an action is opaque. **The mesh
- cannot tell what an action did**, so a network created by one is not a thing the mesh knows
- about — it cannot be reported, counted, or reasoned about, and a module could not require one.
-
-2. **Publish ports on the machine instead.** No new shape, and it works today. **Rejected.** It
- makes a module's internal wiring part of the machine's address space: two modules that each
- want a database on a fixed port collide, and anything else on the machine can reach what was
- meant to be private. It also makes the module's manifest depend on what else is installed,
- which is the thing provisioning exists to remove.
-
-3. **`network` as a ninth shape.** **Adopted.**
-
-## Decision
-
-**`network` joins the vocabulary, and the vocabulary is nine shapes.**
-
-```
-{"id": "internal", "type": "network", "name": "mail"}
-```
-
-**A name and nothing else.** Not a driver, a subnet, an address range or a gateway: every one of
-those is a thing a module would have to know about the machine it lands on, and a module that
-names a subnet is a module that collides with whatever else chose the same one. The runtime picks;
-the mesh names.
-
-**It is created if absent and removed when no longer declared** — an ordinary shape, with the same
-lifecycle as a directory. That is the whole reason it is a shape.
-
-**Declared before the containers that join it.** Resources are applied in the order the module
-wrote them, and orphans are removed in **reverse** — so a network written first is created first
-and removed last, after the containers attached to it are gone. This is not a new rule; it is the
-existing one, and it happens to be exactly right here. A network written *after* its containers
-would fail to remove while they still hold it, and that failure is reported rather than silent.
-
-**What it does not do:** it does not reach across machines. A network is one machine's, like
-everything else the host applies. Modules on different machines reach each other over the private
-network the mesh already provides ([ADR 0007](0007-connectivity.md)), and a shape that tried to
-span machines would be a second overlay with a worse contract.
-
-## Consequences
-
-**The vocabulary is nine, and the count moves with a record.** The test that asserts it names this
-one, so the next person to change it finds the argument rather than a number to edit.
-
-**A compromised control plane can now create and destroy networks on a machine.** Stated plainly
-because that is the cost, and the bound is the point: it can create a named network and remove
-one, and it can do neither to anything it did not declare. It cannot inspect, attach to, or
-reroute what is already there — those would be different shapes, and are not being added.
-
-**A multi-container module becomes expressible**, which unblocks 3.3 and, less obviously, makes
-several smaller modules simpler: anything that is a service plus a sidecar currently has to
-publish a port to talk to itself.
-
-**Nothing is required to use it.** A module of one container declares no network and joins none,
-exactly as now. The substrate keeps using `host`, which is a runtime-provided network and not one
-the mesh creates.
-
-## References
-
-- [ADR 0005](0005-the-node-host.md) — the host's vocabulary, and why each shape is a decision
-- [ADR 0004](0004-a-node-and-how-it-joins.md) — what may be pushed is bounded by form, not by trust
-- [`03-DESIGN/01-to-be/00-work-breakdown.md`](../03-DESIGN/01-to-be/00-work-breakdown.md) — 1.3,
- and the mail system at 3.3 that this is for
diff --git a/02-DECISIONS/0030-data-outlives-the-mesh-that-declared-it.md b/02-DECISIONS/0030-data-outlives-the-mesh-that-declared-it.md
deleted file mode 100644
index a792ee83..00000000
--- a/02-DECISIONS/0030-data-outlives-the-mesh-that-declared-it.md
+++ /dev/null
@@ -1,96 +0,0 @@
----
-topic: the tiers
-status: accepted
-date: 2026-08-31
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0005-the-node-host.md
----
-
-# 30. Data outlives the mesh that declared it
-
-## Context
-
-**The conversion runs on live services holding real data**, and starts on the node that holds all
-of it. Identity, mail, everything. The requirement stated plainly: a data directory may be
-*moved*, and may never be *lost*.
-
-**The host deleted them.** A directory that stopped being declared was an orphan, and an orphan
-directory was removed with `os.RemoveAll` — everything under it — while the report said
-`removed`. A module unassigned took its database's files with it, and nothing anywhere said what
-had been in there.
-
-Reproduced before it was fixed: assign a module, let a service write into its directory, unassign
-the module, and the file is gone.
-
-**A directory stops being declared for ordinary reasons**, which is what makes this sharp rather
-than theoretical. A module unassigned from a node. A manifest edited to move a data folder — the
-exact operation the conversion needs. A resource renamed. A typo. **Every one of those is a normal
-day's work, and every one of them was destructive.**
-
-**The removal order was already right, and that is what makes a fix possible.** Everything the
-mesh puts inside a directory is itself a declared resource, and orphans are removed in reverse
-declaration order — so by the time a directory is reached, what the mesh wrote there is already
-gone. Anything still present was put there by something else.
-
-## Considered Options
-
-1. **A `keep` flag on the directory.** A module declares which of its directories hold data, and
- the host leaves those. **Rejected.** It is safe only when somebody remembered, and the failure
- of forgetting is total and silent. A rule that protects data only when it was asked to is not
- a rule about data, it is a rule about attentiveness — and this is the one place in the system
- where being wrong does not recover.
-
-2. **Never remove a directory.** Simple and unarguably safe. **Rejected**, narrowly: every module
- ever assigned would leave its directories behind for ever, and a machine that accumulates
- things nobody can account for is one where nobody can tell what is still in use. The clean-up
- that is genuinely the mesh's is worth keeping.
-
-3. **Remove a directory only when it is empty.** **Adopted.**
-
-## Decision
-
-**A directory that still holds anything is kept, and the mesh says so.** An empty one is removed.
-
-**This is the host's existing line applied to the one shape where getting it wrong is
-unrecoverable** — *it removes what it made and leaves what it merely configured*
-([ADR 0005](0005-the-node-host.md)). An empty directory is what the host made. A full one is not.
-
-**No flag, no declaration, nothing to remember.** Emptiness is the test, and it is derived from
-the removal order rather than asserted: the mesh's own contents are gone by then, so what remains
-is by definition something nobody declared.
-
-**It is reported, not silent.** The outcome is `kept`, naming how many items are inside and saying
-they are for a person to deal with. A directory quietly left behind is how a machine accumulates
-things nobody can account for — which is the objection to option 2, and it is answered by saying
-so rather than by deleting.
-
-**Files are unchanged.** A declared file is the mesh's own — it wrote it, it owns it, and losing a
-configuration file is not the failure this is about. The distinction is deliberate: **directories
-hold what other things produced; files are what the mesh itself put there.**
-
-## Consequences
-
-**Moving a data directory is now safe by default.** The manifest changes, the old path stops being
-declared, and the data stays where it is until somebody has looked at it. That was the operation
-most likely to destroy something during the conversion, and it is now the operation that does the
-least.
-
-**Unassigning a module leaves its data.** Correct, and it means unassignment is no longer a way to
-clean up — removing data is a person's act, done knowingly. Given what unassignment did before,
-that is the trade being made and it is the right way round.
-
-**A machine can accumulate directories nobody removed.** Accepted, and mitigated by saying so
-every time rather than by a periodic sweep. A sweep would be the deletion this record exists to
-prevent, on a timer, with nobody watching.
-
-**It is not a backup, and must not be mistaken for one.** This stops the mesh destroying data. It
-does nothing about a disk, a mistaken `rm`, or a service corrupting its own store. The conversion
-still needs backups taken and **restored** before anything is moved — a backup nobody has restored
-is a belief, not a copy.
-
-## References
-
-- [ADR 0005](0005-the-node-host.md) — the host removes what it made
-- [`03-DESIGN/01-to-be/00-work-breakdown.md`](../03-DESIGN/01-to-be/00-work-breakdown.md) — the
- conversion this was found by planning
diff --git a/02-DECISIONS/0031-the-control-plane-authenticates-nobody.md b/02-DECISIONS/0031-the-control-plane-authenticates-nobody.md
deleted file mode 100644
index ed6be7ec..00000000
--- a/02-DECISIONS/0031-the-control-plane-authenticates-nobody.md
+++ /dev/null
@@ -1,72 +0,0 @@
----
-topic: the tiers
-status: accepted
-date: 2026-08-31
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0006-the-substrate-and-the-control-plane.md
----
-
-# 31. The control plane authenticates nobody, so identity is a module
-
-## Context
-
-[ADR 0006](0006-the-substrate-and-the-control-plane.md) left one member of the substrate
-conditional, and said exactly why:
-
-| role | product | |
-|---|---|---|
-| identity provider | — | **conditional**: substrate only if the control plane delegates authentication, which is undecided |
-
-[`07-the-foundation.md`](../03-DESIGN/01-to-be/07-the-foundation.md) carried it as an open question —
-*whether identity is the fifth* — noting it followed from a decision nobody had taken.
-
-**The decision is taken: the control plane does not delegate authentication.** There is no mesh
-identity provider.
-
-**Nothing in the mesh's own machinery ever needed one.** A node proves itself with a keypair it
-generated, over a broker account issued at enrolment
-([ADR 0004](0004-a-node-and-how-it-joins.md)). Declarations are verified by signature. None of
-that touches an identity provider, and the conditional was never about machines — it was only ever
-about whether a *person* signing in to a mesh surface would be authenticated by something else.
-
-## Decision
-
-**Identity is a module**, like the mail system and the forge. It runs *on* the mesh, not *of* it
-([ADR 0001](0001-mesh-brokers-nodes-host-agents-think.md)) — a provider other modules require,
-which is the ordinary shape and needs nothing new to express.
-
-**So the substrate is three, and no longer conditional**: a relational store, a message bus, and
-an image registry. Together with
-[ADR 0028](0028-the-substrate-supplies-the-control-plane-and-nothing-else.md), which removed the
-object store, the list is settled and every member is there for the same reason — the control
-plane needs it and cannot ask itself for it.
-
-**A mesh that wants no identity provider runs none.** That is now expressible, and was not while
-it sat in the substrate as a maybe.
-
-## Consequences
-
-**The last open question about substrate membership is closed.** Both halves of ADR 0006's test
-now have an answer for every candidate, and the answer for identity is *the control plane does not
-need it*.
-
-**It does not settle how a person signs in to a mesh surface**, and that is deliberately left
-open. What is settled is that whatever answers it is not part of what must exist before the mesh
-does — so it can be decided late, changed, or replaced, which is precisely what being substrate
-would have prevented.
-
-**It becomes a real test of the module graph.** An identity provider is a module that *other
-modules require* — the object store already consumes it — so it exercises the provider chain more
-seriously than anything ported so far, where the provider was written alongside its consumer.
-
-**Ordering follows from it rather than from preference.** Anything requiring identity has to move
-after it, which is a dependency the graph can state rather than something a person has to
-remember.
-
-## References
-
-- [ADR 0006](0006-the-substrate-and-the-control-plane.md) — the conditional this closes
-- [ADR 0028](0028-the-substrate-supplies-the-control-plane-and-nothing-else.md) — the other member
- removed, and the test applied properly
-- [ADR 0004](0004-a-node-and-how-it-joins.md) — how a node proves itself, which needs none of this
diff --git a/02-DECISIONS/0032-the-local-account-owns-the-mesh.md b/02-DECISIONS/0032-the-local-account-owns-the-mesh.md
deleted file mode 100644
index 81a3ced5..00000000
--- a/02-DECISIONS/0032-the-local-account-owns-the-mesh.md
+++ /dev/null
@@ -1,79 +0,0 @@
----
-topic: how we work
-status: superseded
-date: 2026-08-31
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0031-the-control-plane-authenticates-nobody.md
-superseded-by: 02-DECISIONS/0034-the-local-account-owns-the-mesh.md
----
-
-# 32. The local account owns the mesh; a surface delegates to a module
-
-## Context
-
-[ADR 0031](0031-the-control-plane-authenticates-nobody.md) settled that the control plane
-authenticates nobody, and deliberately left one thing open: **how a person signing in to a mesh
-surface is authenticated.** This answers it, and answers a question 0031 did not ask — *who owns
-the mesh at all.*
-
-**There was no answer, and the absence was invisible** because every operation so far has been run
-by the person sitting at the machine. Nothing had to say whether that was the design or the
-circumstance.
-
-## Decision
-
-**The account that installed the host owns the mesh on that node.** Authority is a local login,
-and there is nothing else to hold.
-
-**No mesh user model.** No accounts, no roles, no grants, nothing to administer. A person with a
-shell on a node can do anything the mesh can do there, because that is already true and pretending
-otherwise would be a boundary that does not exist.
-
-**This follows from what was already decided rather than adding to it.**
-[ADR 0004](0004-a-node-and-how-it-joins.md) says there is no authorisation between nodes — every
-node is the operator's own, so a message from one is a message from them, and *the mesh boundary
-is therefore the security boundary*. A user model inside that boundary would guard nothing: anyone
-who could be stopped by it could equally read the node's key off the disk.
-
-**The board is different, and the difference is the network.** A surface reachable by a browser
-has to know who is asking, because the people reaching it are not, by construction, people with a
-shell on the machine. **So the board delegates to an OAuth provider** — which is a module.
-
-## What this does not change
-
-**The identity provider is still not substrate** (ADR 0031). A *surface* delegating
-authentication is not *the control plane* delegating it. The control plane runs, applies
-declarations and reaches nodes with no identity provider in existence; only the board needs one,
-and only to decide whose browser it is talking to.
-
-The test is unchanged and still answers no: *does the control plane need it in order to run?*
-
-## Consequences
-
-**The board depends on a module, and says so.** An ordinary edge in the graph, which means the
-board cannot come up before the provider it authenticates against — stated as a dependency rather
-than discovered as an outage.
-
-**Moving the identity provider takes the board with it.** During that module's own conversion the
-board is unavailable, and that is acceptable: it is a surface, nothing depends on it, and a brief
-interruption is the trade already accepted everywhere else. Nothing that keeps a service serving
-goes through it.
-
-**Anyone with a shell on a node has full authority there.** Written down rather than left implied,
-because it is the sentence that decides who gets an account on a machine. The protection is the
-machine's own login, and the overlay that keeps the machine unreachable from outside
-([ADR 0007](0007-connectivity.md)).
-
-**A node cannot be operated by somebody without a login on it.** Deliberate, and the cost of
-having no user model: there is no way to give a person authority over one node without giving them
-a shell there. If that is ever wanted, it is a new decision and not a gap in this one.
-
-## References
-
-- [ADR 0031](0031-the-control-plane-authenticates-nobody.md) — the control plane authenticates
- nobody; this answers what it left open
-- [ADR 0004](0004-a-node-and-how-it-joins.md) — no authorisation between nodes, and why the mesh
- boundary is the security boundary
-- [`03-DESIGN/01-to-be/11-a-board.md`](../03-DESIGN/01-to-be/11-a-board.md) — the surface this is
- about
diff --git a/02-DECISIONS/0033-the-substrate-is-a-store-and-a-broker.md b/02-DECISIONS/0033-the-substrate-is-a-store-and-a-broker.md
deleted file mode 100644
index 5de0e075..00000000
--- a/02-DECISIONS/0033-the-substrate-is-a-store-and-a-broker.md
+++ /dev/null
@@ -1,91 +0,0 @@
----
-topic: the tiers
-status: accepted
-date: 2026-08-31
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0028-the-substrate-supplies-the-control-plane-and-nothing-else.md
----
-
-# 33. The substrate is a store and a broker
-
-## Context
-
-Third correction to one table in one day, all found the same way: by asking whether **both** halves
-of the substrate test were actually answered for a given member, or only the second.
-
-The test ([ADR 0006](0006-the-substrate-and-the-control-plane.md)) is *what the control plane needs
-in order to run, and cannot ask itself for, because it is not running yet.* ADR 0006 admits the
-image registry on this line:
-
-| role | product | |
-|---|---|---|
-| image registry | **an OCI registry** | it cannot grant itself a repository |
-
-**That is the second half again.** It is true that a control plane cannot grant itself a
-repository. Nothing establishes that it needs one *in order to run*.
-
-**Counted rather than argued.** `substrate-first-node.lock` — the only bundle there is, and what a
-first node actually becomes — raises twelve resources, and no registry is among them:
-
-```
-container runtime · the store · one database per context · the schemas
-· the broker's certificate · the broker · the control plane
-```
-
-The registry arrives afterwards, as an ordinary module the mesh assigns. That is what the lab
-asserts, in those words: *the mesh runs its own artifact store.*
-
-**ADR 0006 half-said this already**, calling the registry *substrate by role and ordinary by
-delivery, provisioned once there is a control plane to do it.* A member that is provisioned by the
-thing it supposedly precedes is not a member; the phrase was carrying a contradiction rather than
-resolving one.
-
-**The registry is a closer call than the object store, and the difference is worth keeping.** The
-control plane never touches an object store at all — no client, no bucket, ever
-([ADR 0028](0028-the-substrate-supplies-the-control-plane-and-nothing-else.md)). It genuinely
-*uses* the registry: the builder pushes to it, hosts pull from it, and nothing reaches a machine
-without it. **So the registry is a real dependency of the mesh operating, and not of the control
-plane starting** — and it is the second that the word substrate means.
-
-## Decision
-
-**The substrate is two things: a relational store and a message bus.** Both are in the bundle,
-both must exist before the control plane's first instruction, and neither can be asked for.
-
-**The registry is an ordinary module.** The mesh cannot deliver anything without one, and it
-installs one the way it installs everything else. The first node's chicken-and-egg is already
-solved and needs nothing from this list: it fetches upstream images directly, then runs a registry
-of the mesh's own.
-
-**The test is applied to both columns, every time.** *Cannot grant itself one* is true of almost
-any service and settles nothing on its own. It is what admitted the object store, and then the
-registry, and both were removed by asking the other question.
-
-## Consequences
-
-**The substrate is now exactly what the bundle raises**, which is the strongest form this list can
-take: it can be checked by counting rather than by reading an argument. A member that is not in
-the bundle is not substrate, and the two statements cannot drift apart.
-
-**A mesh that builds nothing still needs a registry** — to receive anything at all — but it needs
-it as a module, on its own schedule, replaceable. That was already true and was obscured by the
-list.
-
-**The word may now be doing too little work.** "Substrate" for *a database and a broker* is a term
-of art for two things everybody can name. Renaming is not taken here and is worth considering
-separately; what this record fixes is the membership, not the vocabulary.
-
-**Three removals from one table in one day is itself the finding.** Each member was admitted on the
-half of the test that is easy to answer, and the design read plausibly throughout. The rule that
-comes out of it is not about substrates: **a test with two conditions is a test only when both are
-asked.**
-
-## References
-
-- [ADR 0006](0006-the-substrate-and-the-control-plane.md) — the definition, and the table this
- corrects a second row of
-- [ADR 0028](0028-the-substrate-supplies-the-control-plane-and-nothing-else.md) — the object
- store, removed for the same reason
-- [ADR 0031](0031-the-control-plane-authenticates-nobody.md) — identity, which was conditional and
- is now a module
diff --git a/02-DECISIONS/0034-the-local-account-owns-the-mesh.md b/02-DECISIONS/0034-the-local-account-owns-the-mesh.md
deleted file mode 100644
index 5c14d294..00000000
--- a/02-DECISIONS/0034-the-local-account-owns-the-mesh.md
+++ /dev/null
@@ -1,81 +0,0 @@
----
-topic: how we work
-status: accepted
-date: 2026-08-31
-deciders: jochen
-reconstructed: false
-supersedes: 02-DECISIONS/0032-the-local-account-owns-the-mesh.md
----
-
-# 34. The local account owns the mesh, and a web application's login is not that
-
-*Supersedes [ADR 0032](0032-the-local-account-owns-the-mesh.md), which decided the right thing and
-described it wrongly. The decision below is unchanged; what it said about the board was an
-invention.*
-
-## Context
-
-ADR 0032 answered *who owns the mesh* — the account that installed the host — and then framed the
-board as **a surface that delegates authentication**, a category it made up for the occasion. It
-does not need one.
-
-**The board is a web application.** It has a login, provided by the identity module, in the way
-every web application has a login. That is a fact about an application, not a property of the
-mesh, and giving it a name in the mesh's vocabulary implied a relationship that is not there.
-
-The cost of the invented category was not cosmetic. It made the identity module look like part of
-the mesh's own authority — something the mesh *depends on* to know who anybody is — when the truth
-is that the mesh knows nothing about people at all, and one of the applications running on it has
-a login.
-
-## Decision
-
-**The account that installed the host owns the mesh on that node.** Authority is a local login.
-There is nothing else to hold, no user model, no roles, and nothing to administer.
-
-**This follows from what was already decided.**
-[ADR 0004](0004-a-node-and-how-it-joins.md) says there is no authorisation between nodes — every
-node is the operator's own, so a message from one is a message from them, and *the mesh boundary
-is the security boundary.* A user model inside that boundary would guard nothing: anyone it could
-stop could read the node's key off the disk.
-
-**A web application's login is its own business.** The board authenticates its users through the
-identity module. So might anything else the mesh runs. **None of that is mesh authority**, and the
-mesh does not learn who anybody is from it.
-
-## The line this draws, which is the reason to write it down
-
-**Signing in to an application must not, on its own, become authority over the mesh.**
-
-Today it cannot: the board reads and does not act
-([`11-a-board.md`](../03-DESIGN/01-to-be/11-a-board.md) — *not the way to change things*). Looking
-at a page tells you what is true and changes nothing.
-
-**The moment the board can assign a module, whoever it lets in has mesh authority** — and it would
-arrive as a feature rather than as a decision. That is the failure this record exists to make
-visible, because it is the kind that is only obvious afterwards.
-
-So: **a surface that can change the mesh is a change to who owns the mesh**, and is taken as one.
-Not forbidden — wanting to manage nodes from a browser is reasonable — but not something that
-turns up in a pull request titled *add assign button*.
-
-## Consequences
-
-**The identity module is not special.** Not substrate ([ADR 0031](0031-the-control-plane-authenticates-nobody.md)),
-not part of the mesh's authority, and nothing about the mesh stops working when it is down. Some
-applications cannot be logged into, which is what it means for an application's login provider to
-be unavailable.
-
-**Anyone with a shell on a node has full authority there.** Unchanged from ADR 0032, and still the
-sentence that decides who gets an account on a machine. The protection is the machine's own login
-and the overlay that keeps it unreachable from outside ([ADR 0007](0007-connectivity.md)).
-
-**A node cannot be operated by somebody without a login on it.** The cost of having no user model.
-If that is ever wanted, the paragraph above says what it costs.
-
-## References
-
-- [ADR 0032](0032-the-local-account-owns-the-mesh.md) — superseded; same decision, invented category
-- [ADR 0004](0004-a-node-and-how-it-joins.md) — the mesh boundary is the security boundary
-- [ADR 0031](0031-the-control-plane-authenticates-nobody.md) — the control plane authenticates
- nobody
diff --git a/02-DECISIONS/0035-one-implementation-several-surfaces.md b/02-DECISIONS/0035-one-implementation-several-surfaces.md
deleted file mode 100644
index 722c0769..00000000
--- a/02-DECISIONS/0035-one-implementation-several-surfaces.md
+++ /dev/null
@@ -1,115 +0,0 @@
----
-topic: what runs on it
-status: accepted
-date: 2026-08-31
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0034-the-local-account-owns-the-mesh.md
----
-
-# 35. One implementation, several surfaces, and what that costs
-
-## Context
-
-The mesh is operated from a command line today. It needs to be operable from a browser and from a
-model's tools as well, and the three must not be three different systems.
-
-**The pattern is already in the code and unnamed.** `board` serves HTTP by calling the same
-functions the CLI calls; it holds nothing and decides nothing. What follows makes that the rule
-rather than a property of one command.
-
-**The board is a presentation layer over the control plane.** Not an application beside it holding
-a database credential — the thing that shows what the control plane knows, and asks it to do what
-a person asked for.
-
-## Decision
-
-**The logic lives once, in the context that owns it. A surface is an adapter with no decisions in
-it.**
-
-| surface | for |
-|---|---|
-| **command line** | a person on a machine, and the recovery path below |
-| **HTTP** | the board, and anything else that speaks to the mesh over a network |
-| **model tools** | an agent asking the mesh to do something |
-
-**Every surface refuses identically, because the refusal is not in the surface.** An assignment
-that cannot be satisfied is refused by the same resolution whichever way it arrived. The moment a
-surface can accept something another would reject, the mesh has two answers to one question and
-people learn which to trust.
-
-**Reading and doing are both exposed.** The HTTP surface is not read-only: managing the mesh from
-a browser is the point. This takes the decision
-[ADR 0034](0034-the-local-account-owns-the-mesh.md) said had to be taken deliberately —
-**a browser login now carries authority over the mesh** — and takes it knowingly rather than
-letting it arrive with a feature.
-
-**The networked surfaces authenticate through an OAuth2 identity provider.** Named by protocol
-rather than by product, like every other dependency the mesh takes — AMQP for the bus, S3 for an
-object store, OCI for the registry
-([ADR 0006](0006-the-substrate-and-the-control-plane.md)). What fills the role today is a module
-running Keycloak; what the control plane knows is that it validates a token against a provider
-speaking OAuth2, and replacing that provider is a migration rather than a redesign.
-
-**The command line does not authenticate at all**: it is already behind the machine's own login,
-which is what owns the mesh (ADR 0034).
-
-## What this is not: a kernel every module imports
-
-**The shared library is the failure this project was started over**, and the difference has to be
-stated or it will be rebuilt. The old one is 155 files and 34,636 lines *containing code from
-every context* — work-domain logic sitting in the kernel every module imports, each piece landing
-there to avoid a cycle between two modules that both needed it.
-
-**Shared surfaces are not a shared library.** What is shared here is that three adapters call the
-same functions. Those functions stay in the context that owns them — provisioning's logic in
-provisioning, identity's in identity — and no module imports another's. A surface may call many
-contexts; a context still may not reach into another's store
-([ADR 0008](0008-a-context-owns-its-store.md)).
-
-The test, when something is about to be put "somewhere shared": *does this belong to a context, or
-does it only belong to the surface?* If it belongs to a context it goes there, even if two
-surfaces want it.
-
-## The loop this creates, and the way out
-
-**The control plane's networked surfaces will depend on a module the control plane assigns.**
-An identity provider is an ordinary module ([ADR 0031](0031-the-control-plane-authenticates-nobody.md)). When
-it is down, or being migrated, or misconfigured, the HTTP and tool surfaces cannot authenticate
-anybody — including the person trying to fix it.
-
-**The command line is the way out, and it is why local ownership matters more rather than less.**
-It authenticates through nothing, needs no network, and is available on the machine to the account
-that owns the mesh. **A mesh must always be operable by somebody standing at it.**
-
-So the rule: **no capability exists only behind an authenticated surface.** Anything the board can
-do, the command line can do. That is not a courtesy to CLI users; it is the recovery path, and a
-capability that exists only over HTTP is one that disappears exactly when identity does.
-
-## Consequences
-
-**Identity is still not substrate**, and the test still answers no: the control plane runs, applies
-declarations and reaches nodes with no identity provider in existence
-([ADR 0033](0033-the-substrate-is-a-store-and-a-broker.md)). What is unavailable without it is two
-surfaces, not the mesh.
-
-**Whoever the identity provider admits has authority over the mesh.** That is now a real perimeter
-with real consequences, where before it guarded a page that only read. Who may log in, and to
-which realm, becomes a decision about the mesh rather than about an application.
-
-**A surface must not grow an opinion.** The likely erosion is a validation added to the board
-because it was quicker there — and then the CLI accepts something the board rejects, or worse the
-reverse. Adapters hold no decisions.
-
-**Three surfaces over one implementation is a cost paid three times if it is not one
-implementation.** The reason to write this down now is that the second surface is the cheapest
-moment to get it right, and the third is where the drift usually starts.
-
-## References
-
-- [ADR 0034](0034-the-local-account-owns-the-mesh.md) — the local account owns the mesh, and the
- line this record deliberately crosses
-- [ADR 0001](0001-mesh-brokers-nodes-host-agents-think.md) — the shared library this must not
- become
-- [ADR 0008](0008-a-context-owns-its-store.md) — a context owns its store, which a surface does
- not change
diff --git a/02-DECISIONS/0036-bootstrap-ends-at-a-usable-mesh.md b/02-DECISIONS/0036-bootstrap-ends-at-a-usable-mesh.md
deleted file mode 100644
index 0924818b..00000000
--- a/02-DECISIONS/0036-bootstrap-ends-at-a-usable-mesh.md
+++ /dev/null
@@ -1,99 +0,0 @@
----
-topic: the tiers
-status: accepted
-date: 2026-08-31
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0035-one-implementation-several-surfaces.md
----
-
-# 36. Bootstrap ends at a usable mesh, and the first credential comes from a person
-
-## Context
-
-Bootstrap currently ends when the control plane starts
-([`07-the-foundation.md`](../03-DESIGN/01-to-be/07-the-foundation.md)). That is a mesh that runs and
-cannot yet be used by anybody who is not standing at the machine: the networked surfaces need an
-OAuth2 identity provider ([ADR 0035](0035-one-implementation-several-surfaces.md)), the provider is
-a module, and no module has been assigned.
-
-**So bootstrap should go further** — through the identity provider and the first login — and stop
-at a mesh somebody can actually use.
-
-**One thing in the way, and it is not incidental.** The mesh has never held a readable secret. The
-sealing code says what it does and why:
-
-> Make generates a secret and seals it to both ends, **keeping no readable copy.**
-
-An initial administrator's credential is the first value a **person must read**. Everything else
-the mesh generates is something no human ever sees, and everything a human provides is something
-the mesh immediately stops being able to read.
-
-## Considered Options
-
-1. **The mesh generates it and prints it once**, to the terminal of whoever ran the bootstrap.
- Convenient, and needs no prompt. **Rejected.** It would give the control plane a plaintext
- secret for the first time — briefly, and only to one terminal, but the capability would then
- exist. *An exception made for one case does not stay one*: the next credential that is awkward
- to supply gets printed too, and the property that a copy of the mesh's database is a copy of
- nothing stops being checkable by reading the code.
-
-2. **No password: a one-time link that lets the operator set their own.** The nicest to use.
- **Rejected for now** — it needs a mechanism that does not exist, and the thing it improves is
- one prompt, once, on a new mesh.
-
-3. **The operator supplies it.** **Adopted.**
-
-## Decision
-
-**Bootstrap runs to a usable mesh**: the substrate, the control plane, the identity provider as an
-ordinary module, its realm and client provisioned, an administrator able to log in, and the
-networked surfaces available.
-
-**The administrator's credential is supplied by the person doing the bootstrap**, on standard
-input and not echoed — the path that already exists for a model-access key. The mesh seals it and
-cannot read it afterwards.
-
-**What is created is an account in the identity provider, not a user of the mesh.** The mesh still
-has no user model and gains none here ([ADR 0034](0034-the-local-account-owns-the-mesh.md)). What
-this produces is the first login for the applications that have one.
-
-**The provisioning is ordinary.** A realm, a client and a first account are what an identity
-module's provisioner makes from what the mesh granted it — the same shape as a database and a
-bucket, which are built and proven.
-
-**The surfaces arrive when their dependency does.** The command API is not started with the
-control plane and then broken until identity exists; it becomes available once it can authenticate,
-the way anything else waits for a provider.
-
-## Consequences
-
-**The mesh still never holds a readable secret**, and that sentence needs no exception clause.
-That is the whole reason for the prompt.
-
-**An unattended bootstrap is still possible, and the value still comes from outside.** Automation
-supplying the credential is the operator supplying it. What is refused is the *mesh inventing*
-one — so an unattended bootstrap with no credential provided produces a mesh with no
-administrator, which is correct rather than broken.
-
-**Bootstrap gains an interactive step**, and it is the only one. Worth stating because a bootstrap
-that cannot run without a person is a real constraint on how a node is stood up, and this is
-deliberate rather than an oversight.
-
-**The identity provider is still not substrate.** It is assigned by the control plane, so it comes
-after it, and a thing that comes after cannot be a thing that must exist before
-([ADR 0033](0033-the-substrate-is-a-store-and-a-broker.md)). Bootstrap running through it does not
-move it: bootstrap is a sequence, the substrate is a dependency.
-
-**And the recovery path is unchanged.** When the identity provider is broken later — which is the
-failure that matters, not the one at first start — the command line still works, because it
-authenticates through nothing (ADR 0035).
-
-## References
-
-- [ADR 0035](0035-one-implementation-several-surfaces.md) — the surfaces, and why the command line
- must keep working
-- [ADR 0034](0034-the-local-account-owns-the-mesh.md) — the local account owns the mesh; this adds
- no user model
-- [ADR 0033](0033-the-substrate-is-a-store-and-a-broker.md) — what must exist before the control
- plane, which this does not change
diff --git a/02-DECISIONS/0037-where-a-module-lives.md b/02-DECISIONS/0037-where-a-module-lives.md
deleted file mode 100644
index c01e6c65..00000000
--- a/02-DECISIONS/0037-where-a-module-lives.md
+++ /dev/null
@@ -1,119 +0,0 @@
----
-topic: building it
-status: accepted
-date: 2026-09-01
-deciders: jochen
-reconstructed: false
-rests-on: 02-DECISIONS/0009-modules-and-the-graph.md
----
-
-# 37. Where a module lives
-
-## The question
-
-The mesh's own module descriptions currently sit in `examples/` inside the control plane, beside
-the small programs that hand out logins. That was fine while there were three of them. It is
-wrong now, and the name is doing active harm: everything in `examples/` reads as a sketch, and one
-of them shipped naming a container image that nothing in the repository builds. A directory called
-*the catalogue* would have made *does this actually work* the obvious question to ask of it.
-
-So: **one repository holding the modules we ship?** And if so, where does everything that is not
-ours go?
-
-## What a module actually is, counted
-
-The system being replaced has **126 modules** on its main branch. The shape of them is the whole
-argument, so it is measured rather than assumed:
-
-| | count | what it is |
-|---|---|---|
-| **the module is software** | 47 | its own source tree lives inside the module — a daemon, a service, a library |
-| **helper scripts only** | 44 | no application of its own; scripts it runs at install time or offers to an agent |
-| **a description and nothing else** | 35 | a package to install and some files to write |
-
-**Two thirds of modules contain code.** The largest is a shared library of 182 source files. A
-speech-capture module carries a complete daemon — audio capture, mixing, transcription, a model
-runner. Treating a module as *a description of something else* is true of barely a quarter of them.
-
-That kills the simplest answer. A catalogue cannot be "a folder of manifests" when most modules
-are programs.
-
-## The four kinds, which want different homes
-
-**1. What the mesh is made of.** The control plane, the host, the shared library, the board.
-These are not modules that happen to be ours; they are the mesh, expressed as modules so it can
-install itself. They belong in the repositories that build them, which already exist.
-
-**2. Something the world made, that we describe.** A forge, a mail system, an identity provider,
-a media server. Nobody upstream ships a description; somebody has to write one, and it is the same
-description for everybody who runs it. **This is what a catalogue is for.** It is also where the
-small programs that create accounts belong, because such a program is part of describing that
-service, not part of the mesh.
-
-**3. Something we wrote, that runs somewhere.** An application, a site, a side project. The
-description belongs **with the code, at the root of its own repository**, because the two change in
-the same commit. A repository that gains an environment variable and a description that gains it
-elsewhere will drift, and there is no mechanism that could stop it. This is already how it works
-and it should stay that way.
-
-**4. A package and some files.** A tool, a font, a shell. Thirty-five of these, and each is a few
-lines. The catalogue.
-
-## The proposal
-
-**A `mesh-catalog` repository** holding kinds 2 and 4: descriptions of software we did not write,
-and the programs that provision it. Not kind 1, which is the mesh itself. Not kind 3, which lives
-with its own code.
-
-**The mesh's list of modules is not this repository.** It is a table in the control plane, filled
-by adding a description to a running mesh. The catalogue is a *source* to add from — one of
-several, and the mesh already records which: every module carries where it came from, the branch
-followed there, and the commit its description was read at. **Nothing needs inventing to support
-modules from anywhere**; a repository of our own is simply the source we curate.
-
-**A description is checked by the tool, not by a test that imports the tool.** Today a test in the
-control plane parses the example manifests by reaching into the control plane's internals, and
-another reads the control plane's own build file to check every image a module names can be built.
-Two jobs tangled. A `module check` command on the control plane's binary would let the catalogue
-hold data validated from outside, and would give the same check to somebody describing their own
-application in their own repository — which is the case that matters most and currently has no
-check at all.
-
-## What this costs, and the argument against
-
-**It is early.** Ten modules exist, four of them ours. Moving ten files is a morning; moving a
-hundred is a week — but the hundred is not here yet, and splitting now adds a second repository to
-release across before there is anything to release.
-
-The counter is that the tangle is already producing faults rather than merely threatening to. A
-manifest naming an unbuildable image, and a test reading a build file two directories up, are both
-symptoms of one repository doing two jobs. And the moment the first module is adopted on a real
-machine, the descriptions stop being examples and become the thing deployments come from. **That
-is the moment this becomes urgent, and it is close.**
-
-## What it does not settle
-
-**Where a provisioning program's image is published**, and how a description pins it. A description
-names an image by digest; the image is built from the catalogue; the catalogue must therefore both
-produce an image and refer to it, which is the same knot the bootstrap has and solves by writing
-the digest down after building.
-
-**Whether kind 4 deserves a module at all.** Thirty-five descriptions that say *install this and
-write these files* may be better as one module with settings than as thirty-five modules. Left
-open deliberately; it is a question about the shape of the catalogue, not about whether to have one.
-
-## Accepted, 2026-09-29, against what was built
-
-*Marked in a grooming pass: the mesh was built to this record and the record still said `proposed`.*
-
-The proposal is the arrangement that exists. `mesh-catalog` holds descriptions of software we did
-not write and the programs that provision it, and holds neither the mesh's own components nor an
-application's own module. The mesh's list of modules is a table in the control plane, filled by
-`module add`, and every module records the source it came from with the commit it was read at.
-
-**One half is not built: `module check` as a command on the control plane's binary.** A manifest is
-still validated by a test that reaches into the control plane's internals — which works for this
-catalogue and gives nothing at all to somebody describing their own application in their own
-repository, which this record says is the case that matters most. That is
-[issue 148](../04-ISSUES/148-a-manifest-outside-this-catalogue-has-no-check/00-report.md).
-
diff --git a/02-DECISIONS/0038-the-mesh-assigns-the-port.md b/02-DECISIONS/0038-the-mesh-assigns-the-port.md
deleted file mode 100644
index c8cb2697..00000000
--- a/02-DECISIONS/0038-the-mesh-assigns-the-port.md
+++ /dev/null
@@ -1,82 +0,0 @@
----
-topic: what runs on it
-status: accepted
-date: 2026-09-01
-deciders: jochen
-reconstructed: false
-rests-on: 02-DECISIONS/0009-modules-and-the-graph.md
----
-
-# 38. The mesh assigns the port, and a module does not care
-
-## The problem, as met
-
-A database module cannot start on a machine that runs the control plane. The mesh keeps its own
-store there and holds 5432; the module publishes 5432. Nothing notices until a container runtime
-three layers down says `port is already allocated`
-([`028`](../04-ISSUES/028-two-things-want-one-port-and-nothing-says-so/00-report.md)).
-
-A module cannot fix this by choosing better, because **a module cannot know what else is on the
-machine.** It is written once and assigned anywhere. Any number it picks is a guess about a
-machine it has never seen, and two modules guessing the same number is not a mistake either of
-them made.
-
-## The number is written three times, and nothing makes them agree
-
-Every module says its port in three places:
-
-| where | for | example |
-|---|---|---|
-| `listens` | the rule set that lets traffic in | `{port: 5432, from: mesh}` |
-| `serves` | what a consumer must know to connect | `{port: 5432}` |
-| a container's `ports` | what the runtime publishes | `"5432:5432"` |
-
-They agree today because one person wrote all three. Nothing checks it. A module whose `serves`
-said 5432 and whose container published 5433 would resolve, compose, apply, and hand every
-consumer a port that answers nothing.
-
-## The decision
-
-**The mesh assigns the machine-side port, and the module says only what it needs.** A module
-declares that a container port must be reachable and what it is for. Which number the machine uses
-is the mesh's to choose, because the mesh is the only thing that knows what else is there.
-
-**One source, and the other two are derived.** `serves` carries the assigned port so a consumer is
-told where to connect without the module having written it down; the rule set is computed from the
-same assignment. Three copies become one fact.
-
-**An assignment is made once and kept**, exactly as a credential is. A port that moved on every
-push would restart both ends each time and would hand consumers a number that was true when it was
-read.
-
-## Some ports cannot move, and that is a claim
-
-Mail is 25, submission is 587, IMAP over TLS is 993. A mail system on a strange port is not a mail
-system. So a module may say a port is **fixed by the protocol** rather than assigned.
-
-**A fixed port is exactly a claim** — the thing the mesh already has for what is singular on a
-machine: one seat, one display server, one artifact store. Two modules wanting 25 on one machine is
-the same shape as two wanting the seat, and gets the same answer: the second is refused, by name,
-when it is assigned rather than when it is applied.
-
-That is why this does not need a new mechanism so much as it needs the existing one pointed at
-ports.
-
-## What follows
-
-- **A module becomes portable in a way it was not.** Two databases on one machine stop being a
- collision and become two assignments.
-- **The substrate has to be visible.** The mesh cannot assign around its own store while it has
- never heard of it. What the bundle holds must be written down somewhere the assignment can read
- — which the bundle does not say today.
-- **A refusal can be useful.** *25 is held by the mail system on this machine* is a sentence a
- person can act on. `port is already allocated` is not.
-- **`serves` stops being written by hand**, which is a small vocabulary change with a large
- consequence: what a consumer is told is now derived from what actually happened.
-
-## What this does not settle
-
-**Whether a module should publish to the machine at all.** Assignment makes publishing safe; it
-does not make it necessary. Consumers could instead reach a provider on the module's own network by
-name, with nothing published — which would make the question moot for anything inside the mesh, and
-would still leave it for anything reached from outside.
diff --git a/02-DECISIONS/0039-what-the-sdk-holds-and-refuses.md b/02-DECISIONS/0039-what-the-sdk-holds-and-refuses.md
deleted file mode 100644
index 27637a55..00000000
--- a/02-DECISIONS/0039-what-the-sdk-holds-and-refuses.md
+++ /dev/null
@@ -1,108 +0,0 @@
----
-topic: building it
-status: accepted
-date: 2026-09-03
-deciders: jochen
-reconstructed: false
----
-
-# 39. What the SDK holds, and what it refuses
-
-> **The mechanism changed — 2026-10-02, by [ADR 0188](0188-a-modules-own-code-is-bundles-in-any-language-and-a-tools-bundle-speaks-mcp-to-the-runtime.md).** The test of this record — frequent *and* cascading does not belong — stands and now applies to one SDK per language. Where *what it holds* names the broker client and the event consumer, read: the local protocol a tools bundle speaks to the node's runtime; the transport lives in the runtime and in no SDK, which is what keeps a bus change from rebuilding any module in any language.
-
-_Reconciliation note (2026-09-05): supersedes the earlier "repository structure" decision, which the consolidation folded; no standalone record remains to point at, so body references to it now point at the nearest surviving record, [ADR 0015](0015-applications-live-in-their-own-repository.md)._
-
-## Context
-
-The earlier "repository structure" decision (folded in consolidation; see the reconciliation note
-above, and [ADR 0015](0015-applications-live-in-their-own-repository.md) as the nearest survivor)
-named `mesh-sdk` "contracts shared across tiers:
-types, not behaviour." That line is superseded here, because it draws the boundary in the wrong
-place. The boundary that matters is not *types versus behaviour* — it is **how often the thing
-changes**.
-
-The current SDK is the cautionary tale, and its failure is precise. `hal/sdk` holds all the
-code, including a per-module API client for every service (`clients/plex.ts`, `clients/gitea.ts`,
-…) and a per-module tool implementation for each (`tools/plex.ts`, …). Every module depends on
-the SDK, so **every edit to any of that per-module code rebuilds every module** — the cascade.
-The SDK is under constant maintenance precisely because it became the place all the volatile
-per-module logic accumulated.
-
-The root cause is worth stating exactly, because the fix follows from it: the pressure was never
-to share a client *between* modules. It was to share a client between one module's *own features*
-— plex's tools, its health check and its hooks all wanted the same `PlexClient` — and the only
-place to share code across a module's features was the global SDK. So **intra-module sharing
-leaked out as inter-module coupling.**
-
-## Decision
-
-The SDK holds the **stable spine** that modules build against, and earns its place by rarely
-changing. The test for membership is change-frequency, not kind.
-
-### What it holds
-
-- The **tool-serving harness** — the worker and registration mechanism, and the tool-definition
- type. *How* a tool is declared and served is settled; it does not change when an individual
- tool does.
-- The **messaging and event framework** — the broker client, the event consumer, the envelope.
-- The **contracts** — the manifest, declaration, provision and link shapes.
-- **Core primitives** — sealing and crypto, semver, the shared resolution helpers.
-
-These change rarely and deliberately. When one of them does change, a rebuild of everything is
-the *correct* outcome, because the contract every module shares has genuinely changed.
-
-### What it must not hold — the more important half
-
-- **A module's API client.** A Plex client, a Gitea client, a MinIO client belong in their
- module. They change when that service's API or the module's use of it changes, which is often,
- and which has nothing to do with any other module.
-- **A module's tool implementations.** Same reason, same place: in the module.
-- **Anything volatile** — anything that changes when one service's features change.
-
-The rule, stated so it can be applied without re-deriving it:
-
-> If editing a thing recompiles unrelated modules **and** it changes often, it does not belong
-> in the SDK.
-
-Both conditions are load-bearing. A rare change that cascades is fine — that is a contract, and
-the cascade is correct. A frequent change that stays local is fine — that is a module minding its
-own business. Only **frequent *and* cascading** is the disease, and per-module clients and tools
-are its carriers.
-
-### Where per-module shared code lives instead
-
-Code shared among a module's *own* features lives **in the module**. The default is the plainest
-thing that works: an ordinary shared file the features import — `plex/client.ts`, imported by
-`plex/tools/`. Within one module, features are files importing sibling files; no package
-boundary, no ceremony.
-
-A **module-local SDK** (a sub-package with its own version) is warranted only for the few modules
-whose shared surface is large enough to version on its own. It is the exception, not the shape.
-
-Either form gives the property the global SDK could not: editing a module's shared code rebuilds
-**that module and nothing else**.
-
-## Consequences
-
-- The cascade becomes **structurally impossible for module logic**. There is no longer an edge
- from one module's internals to another, so the only thing that can rebuild everything is a real
- change to a shared contract in the SDK — which is rare, and when it happens, is right.
-- The SDK is small and stable **by construction**, not by discipline. Its size is no longer a
- thing anyone has to police.
-- **Converting a module from the current system is partly a de-coupling, not just a move.** Its
- client and its tools are pulled *out* of the shared SDK and *into* the module. A conversion
- that copied `clients/plex.ts` into the SDK's replacement would rebuild the exact mistake.
-- The host still does not import the SDK. It depends on nothing
- ([ADR 0005](0005-the-node-host.md)) and **mirrors** the contracts rather than
- importing them, exactly as its apply-shapes table already does deliberately. The SDK is shared
- by the tiers that *can* share code; the host is not one of them.
-
-## References
-
-- The earlier "repository structure" decision — named the repositories; its `mesh-sdk`
- description ("types, not behaviour") is superseded by this record (folded in consolidation;
- nearest survivor [ADR 0015](0015-applications-live-in-their-own-repository.md)).
-- [ADR 0001](0001-mesh-brokers-nodes-host-agents-think.md) — the decomposition this serves: code
- belongs to the boundary that owns it.
-- [ADR 0005](0005-the-node-host.md) — why the host mirrors the contracts instead of
- importing the SDK.
diff --git a/02-DECISIONS/0040-what-a-module-is.md b/02-DECISIONS/0040-what-a-module-is.md
deleted file mode 100644
index 8b025787..00000000
--- a/02-DECISIONS/0040-what-a-module-is.md
+++ /dev/null
@@ -1,103 +0,0 @@
----
-topic: what runs on it
-status: accepted
-date: 2026-09-03
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0009-modules-and-the-graph.md
----
-
-# 40. What a module is
-
-_Reconciliation note (2026-09-05): supersedes the earlier "grouped by domain" decision, which the consolidation folded into how-we-build.md; no standalone record remains to point at._
-
-## Context
-
-[ADR 0009](0009-modules-and-the-graph.md) settled that everything is a module, but never said what a
-module *is* beyond "a directory the mesh processes." That gap let the catalogue's breadth read as a
-smell: a module can carry a container, a built image, tools, a provisioner, migrations, health,
-config, seat claims, requires and provides — so much that the unit seemed ill-defined.
-The earlier "grouped by domain" decision (folded in consolidation; see the reconciliation note
-above) tried to organise modules by domain, which is the wrong axis. This record states what a module is, drawn from the cases that
-stress-tested it: the shell, i3-vs-sway, umami, and "database."
-
-## Decision
-
-**A module is one self-contained piece of software the mesh installs and manages** — everything
-needed to make that one thing real and integrable: what runs, the seats it claims, what it provides
-to other modules, what it requires from them, and what operates it.
-
-The **software is the module's identity.** Capabilities, seats and provisioned resources are the
-**relationships *between* modules**, not what a module is — and that is what binds a module into one
-thing. umami is bound by *being umami*: its container runs umami, its provisioner creates umami sites,
-its tools query umami, its `requires` gets umami a database. Every feature serves the one software.
-
-### The three relationships
-
-1. **Shared seat** — several modules fulfil a capability and coexist; one may be default. bash, zsh
- and fish all join `shell`.
-2. **Exclusive seat** — modules contend for a single slot; one holds it. i3 (needs x11) and sway
- (needs wayland) contend for `display-session`.
-3. **Provide / require** — a provider ships the **provisioner** that creates instances of the
- resource it offers and returns sealed credentials; a consumer requires it and the mesh wires the
- credential in. Symmetric: umami requires a database *and* provides analytics.
-
-### Interfaces are mesh-owned; providers adapt to them
-
-The mesh **defines the interface** for a capability — the provider-neutral contract of what a
-consumer receives and how it integrates. Both sides conform: a provider's provisioner **adapts** its
-software's real API to the mesh contract; a consumer depends on the **interface**, never on a
-provider. Swap one provider for another and the consumer does not change.
-
-### The naming rule — draw the interface at the consumer's real coupling
-
-Name a `provides`/`requires` at the **widest boundary across which the consumer genuinely does not
-care which implementation serves it**:
-
-- Where the consumer's coupling is thin — an analytics embed snippet and dashboard, opaque to it —
- the mesh defines a neutral interface (`analytics`) and providers (umami, amumi) adapt. Swappable
- across vendors.
-- Where the consumer **speaks a protocol** — a database's wire protocol and query dialect — the
- interface *is* the protocol: `postgres-database`, `mssql-database`, `mongodb-database`. Swappable
- only among protocol-compatible implementations, **never across**, because the application cannot
- cross it either. "database" is not a capability; the protocol is.
-- **Never false genericity.** A name must not promise a swap the contract cannot deliver
- ([research 005](../01-RESEARCH/005-domain-grouping/analysis.md)).
-
-This is [ADR 0027](0027-a-provision-names-what-the-consumer-is-coupled-to.md)'s rule made general — "names the protocol, not
-the product; a database names the engine because the app targets it" — with the reason stated: the
-contract sits where the coupling is.
-
-### What is not a module
-
-- A **library** (built against, never deployed — [ADR 0039](0039-what-the-sdk-holds-and-refuses.md)).
-- A **control-plane context** (the mesh itself — [ADR 0001](0001-mesh-brokers-nodes-host-agents-think.md)).
-
-A **swappable machine mechanism** (a firewall — ufw, nftables) *is* a module implementing a
-capability. The host hardcodes no firewall, supervisor, package manager or runtime; it owns only the
-generic apply primitives and platform detection, so it runs where none of those exist — an Android
-phone has no ufw, systemd, pacman or Docker.
-
-> **The mechanism changed — 2026-10-02, by [ADR 0176](0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md).** The shell example above — *bash, zsh and fish all join `shell`; one may be default* — is read as *installed is not holding*: the three may all be installed, and the `login-shell` seat is node-scoped and held by exactly one. The decision — what a module is, and the three relationships — stands; [ADR 0173](0173-the-operators-machine-is-the-meshs-and-a-module-is-what-it-declares.md) applies it to the operator's whole machine.
-
-## Consequences
-
-- **Supersedes the earlier "grouped by domain" decision** (folded in consolidation; see the
- reconciliation note above). Modules are
- organised by their relationships (seats, provisions), not grouped into domain folders.
-- **Refines [ADR 0009](0009-modules-and-the-graph.md).** Everything the mesh runs and integrates is
- a module — but a module is defined by the *software it delivers*, not by being a bucket of features.
-- The target is a **self-fulfilling mesh**: declared wants bound to swappable modules, provisioners
- wiring credentials, nothing hardcoded. The control plane's whole job is the binding.
-- Converting a module from the old system includes pulling its per-module code out of the shared SDK
- ([ADR 0039](0039-what-the-sdk-holds-and-refuses.md)) and shipping its provisioner as an adapter to a
- mesh interface — a de-coupling, not just a move.
-
-## References
-
-- [ADR 0009](0009-modules-and-the-graph.md) — everything is a module; this says what one is.
-- [ADR 0001](0001-mesh-brokers-nodes-host-agents-think.md) — contexts are the mesh, not modules.
-- The earlier "grouped by domain" decision — superseded (folded in consolidation; see the note above).
-- [ADR 0027](0027-a-provision-names-what-the-consumer-is-coupled-to.md) — protocol-not-product, generalised here.
-- [ADR 0039](0039-what-the-sdk-holds-and-refuses.md) — per-module code lives in the module.
-- [research 011](../01-RESEARCH/011-the-module-graph/00-overview.md) — the graph of these relationships.
diff --git a/02-DECISIONS/0041-events-are-a-relationship.md b/02-DECISIONS/0041-events-are-a-relationship.md
deleted file mode 100644
index 83bf0b10..00000000
--- a/02-DECISIONS/0041-events-are-a-relationship.md
+++ /dev/null
@@ -1,101 +0,0 @@
----
-topic: what runs on it
-status: accepted
-date: 2026-09-03
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0040-what-a-module-is.md
----
-
-# 41. Events are a relationship, the lighter sibling of provisioning
-
-## Context
-
-[ADR 0040](0040-what-a-module-is.md) names two relationships between modules — seats and
-provide/require (provisioning). A third is latent in the mesh and worth making first-class: the
-broker every node already runs ([ADR 0002](0002-nodes-communicate-over-a-broker.md)) can carry a
-module's activity as **events**, which any other module reacts to. A logger that writes an audit
-trail, a module that acts when another module acts, observability — all of it is one mechanism, and
-today it is ambient rather than declared.
-
-## Decision
-
-**A module emits events and consumes events, and both are declared** — parallel to `provides` /
-`requires`, so the mesh knows the event graph the same way it knows the provisioning graph.
-
-### Events are provisioning's lighter sibling
-
-| | provisioning | events |
-|---|---|---|
-| shape | **1:1**, a provider creates a resource *for* one consumer | **1:many**, a module emits, any number listen |
-| credential | yes — sealed, per consumer | none — it is broadcast |
-| machinery | a provisioner (the reconcile adapter) | nothing but the broker's topic routing |
-| declared as | `provides` / `requires` | `emits` / `consumes` |
-
-Because an event is broadcast and credential-free, there is no provisioner and no per-consumer
-setup — only a subscription. That is why it is the *lighter* relationship, and why most
-inter-module reaction should be an event, not a provision.
-
-### An event carries what an audit needs
-
-Every event carries its **type** (a dotted topic key, so listeners match by prefix), its **source**
-module, the **node** it came from, and the **time**. A body follows. The metadata is not optional:
-a reaction may only need the body, but an audit trail needs to know who did what, where and when,
-and an event that cannot answer that is not auditable.
-
-### The audit logger is just a consumer of everything
-
-A logger that records the whole mesh's activity is **not a privileged component** — it is an
-ordinary module that consumes `#` (every event) and writes them down. It holds no special access;
-it only listens widely. That it falls out of the model with no new machinery is the check that the
-model is right.
-
-### `consumes` is validated like `requires`
-
-A `consumes` for an event that **nothing** `emits` is a dangling edge, and the mesh refuses it
-before deploy — the same rule that catches a `requires` for a resource nothing provides
-([research 011](../01-RESEARCH/011-the-module-graph/00-overview.md)). A listener waiting for an
-event that can never arrive is a silent failure, and this repository's whole discipline is against
-silent failure.
-
-### One runtime serves all three
-
-The per-node module runtime that serves a module's tools also wires its `consumes` (subscribe,
-dispatch to the handler) and lets its code `emit`. Tools are *invoked* (request/reply), resources
-are *provisioned* (1:1, credentialed), events are *emitted and consumed* (1:many, broadcast) —
-three relationships, one broker, one runtime, all declared on the manifest.
-
-## Consequences
-
-- The mesh gains a declared **event graph** alongside the provisioning graph — visible, validated,
- reasoned over.
-- **Reaction becomes the default coordination**: a module acts on another's event without either
- knowing the other, and without a credentialed link. Coupling drops.
-- An **audit trail** is a module, not a platform feature — and can be swapped, extended or run more
- than once (a file logger and a queryable one) with no change to anything that emits.
-- The runtime must dispatch a module's event handlers as well as its tools; that generalisation is
- small (both arrive by importing the module's entrypoint) but it is real work.
-
-## Progressive insight
-
-> **Progressive insight — 2026-09-26.** *"No provisioner and no per-consumer setup" was a fact
-> about the transport, and the transport changed.* This record's table says an event's machinery is
-> "nothing but the broker's topic routing", and the text that an event needs "no per-consumer setup
-> — only a subscription". That was true of a topic exchange, where a binding cost nothing and the
-> broker fanned out. On NATS
-> ([ADR 0106](0106-the-bus-is-nats.md)) a subscription is a **durable consumer**: a real object
-> with a name, an ack policy, a delivery limit and its own ack subject, created when a module is
-> assigned and removed when it is not. Per-consumer setup exists, and the controller does it.
->
-> The decision is untouched — events are declared on both sides, 1:many, credential-free, and
-> still provisioning's lighter sibling; the lightness is now relative rather than absolute.
-> [ADR 0126](0126-a-module-declares-its-own-seats.md) adds the relationship this record's two
-> columns had no room for: work addressed to a role, where exactly one holder must act.
-
-## References
-
-- [ADR 0002](0002-nodes-communicate-over-a-broker.md) — the broker events ride.
-- [ADR 0040](0040-what-a-module-is.md) — the relationships this extends.
-- [ADR 0039](0039-what-the-sdk-holds-and-refuses.md) — `emit`/`on` are stable sdk surface; the
- broker binding and the runtime are not.
-- [research 011](../01-RESEARCH/011-the-module-graph/00-overview.md) — the graph these edges join.
diff --git a/02-DECISIONS/0042-the-shape-of-an-event-on-the-wire.md b/02-DECISIONS/0042-the-shape-of-an-event-on-the-wire.md
deleted file mode 100644
index d5633851..00000000
--- a/02-DECISIONS/0042-the-shape-of-an-event-on-the-wire.md
+++ /dev/null
@@ -1,116 +0,0 @@
----
-topic: what runs on it
-status: accepted
-date: 2026-09-03
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0041-events-are-a-relationship.md
----
-
-# 42. The shape of an event on the wire
-
-## Context
-
-[ADR 0041](0041-events-are-a-relationship.md) made events a relationship — `emits`/`consumes`, the
-graph, the audit logger. It did not say what an event *is* on the broker: the exchanges, the
-routing keys, the headers, the queues and their configuration. That shape is a contract every
-emitter and consumer conforms to, exactly as [ADR 0010](0010-delivery.md)
-is for declarations — and it was being decided ad-hoc in code. This settles it, so the sdk and the
-runtime implement one contract and a module never reinvents it.
-
-## Decision
-
-### Two exchanges, kept apart
-
-- **`mesh.events`** — a durable topic exchange. Every event rides it: module, mesh and node.
-- **`mesh.rpc`** — a durable topic exchange. Tool invocations (request/reply) ride it.
-
-Kept separate because RPC is not an event: a `#` subscription on `mesh.events` is then a complete
-audit of what happened, with none of the invocation traffic.
-
-### The routing key is the event type, namespaced by origin
-
-Dotted and hierarchical — `..` — with three reserved origins:
-
-- `module..` — `module.umami.site.created`
-- `mesh..` — `mesh.delivery.deployed`, `mesh.provisioning.granted`
-- `node..` — `node.anchor.joined`, `node.anchor.unreachable`
-
-Topic matching gives a consumer `node.*.joined`, `module.umami.#`, or `#`. The origin roots are
-reserved; everything after is the emitter's own namespace.
-
-### Metadata in headers, payload in the body
-
-An event's identity and provenance are AMQP **headers**, so a consumer — or the broker, or an
-audit tool — reads who/when/what without parsing the body, and the body is only the domain payload.
-
-**Required headers**
-
-| header | meaning |
-|---|---|
-| `x-event-id` | a unique id — for dedup and audit (delivery is at-least-once, below) |
-| `x-source` | the emitter: the module, context or node name |
-| `x-node` | the node it was emitted from |
-| `x-time` | emit time, RFC-3339 |
-| `content-type` | `application/json` |
-
-**Optional headers**
-
-| header | meaning |
-|---|---|
-| `x-causation-id` | the event or command that caused this one — tracing |
-| `x-schema` | a version of the body's shape, so a body evolves without silent misreads |
-
-The routing key already carries the type; it is not duplicated as a header. An **unknown `x-`
-header is ignored, not refused** — unlike a declaration, an event is observed by parties that need
-not all understand every header, and refusing would couple every consumer to every emitter's
-additions.
-
-### Messages are persistent
-
-Events are published persistent (delivery-mode 2). An audit trail that loses events on a broker
-restart is not one, and the cost is disk the broker already spends on everything durable.
-
-### Queues: one per consumer, durable, dead-lettered
-
-- **A consumer's queue** is `..events`, durable, bound to that module's consumed
- patterns. Durable so a restart does not drop what arrived while it was down. **Manual ack** after
- the handler succeeds — at-least-once.
-- **Prefetch** bounds in-flight work (default 32) so one slow consumer does not pull the whole
- backlog into memory.
-- **A dead-letter exchange** `mesh.events.dead` receives a message rejected past a redelivery limit,
- so a poison event is set aside for inspection rather than looping forever or vanishing silently.
-- **The audit logger's queue** `.audit-logger.events`, bound to `#`, is the same shape —
- durable, persistent, dead-lettered — because completeness is its whole job.
-- **RPC reply queues** are exclusive, auto-delete and server-named; **RPC serve queues**
- `serve.` are durable and shared, so several runtimes serving one tool key compete rather than
- each answer.
-
-### At-least-once, and consumers are idempotent
-
-A handler may see an event twice — a redelivery after a crash between doing the work and acking.
-Consumers must be idempotent, and `x-event-id` is what makes dedup possible. **Exactly-once is not
-offered**: it is a promise no broker keeps honestly, and saying so is better than pretending.
-
-## Consequences
-
-- The event shape is a versioned, enforced contract, not conventions each module reinvents. The
- sdk's `emit`/`on` and the runtime's AMQP binding implement it; a module never sees an exchange or
- queue name.
-- Metadata-in-headers means the body is exactly the domain payload, and a consumer that only wants
- provenance never parses it.
-- Adding a header or an origin root widens the contract and is reviewed as one — the discipline
- [ADR 0010](0010-delivery.md) applies to the
- declaration vocabulary.
-- The sdk's first cut carried source/node/time in the *body*; this supersedes that — they move to
- headers. That is code to align, in `mesh-sdk` (`emit`/`on`) and `mesh-tools` (the binding, queue
- config, dead-letter).
-
-## References
-
-- [ADR 0041](0041-events-are-a-relationship.md) — events as a relationship; this is their wire shape.
-- [ADR 0010](0010-delivery.md) — the precedent: a wire
- contract, versioned, additions reviewed as security.
-- [ADR 0002](0002-nodes-communicate-over-a-broker.md) — the broker.
-- [ADR 0039](0039-what-the-sdk-holds-and-refuses.md) — `emit`/`on` are stable sdk surface; the
- binding, queue config and dead-letter are the runtime's, not the sdk's.
diff --git a/02-DECISIONS/0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md b/02-DECISIONS/0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md
deleted file mode 100644
index 1acfab82..00000000
--- a/02-DECISIONS/0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md
+++ /dev/null
@@ -1,101 +0,0 @@
----
-topic: what runs on it
-status: accepted
-date: 2026-09-04
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0041-events-are-a-relationship.md
----
-
-# 43. A module's broker account is scoped by what it emits and consumes
-
-## Context
-
-[ADR 0041](0041-events-are-a-relationship.md) made events a relationship — `emits` and `consumes`
-on the manifest. [ADR 0042](0042-the-shape-of-an-event-on-the-wire.md) gave them a wire shape — the
-`mesh.events` exchange, the durable per-consumer queue, the reserved routing-key origins. Neither
-said how a module *reaches* the broker: what account it holds, and what that account is allowed to
-do.
-
-As the code stands, there is no answer. The mesh can provision a **node** account (at enrolment)
-and a **builder** account (scoped to the build queue), and it can *deliver* any module a sealed
-own-secret at a declared path — but it has no way to provision a broker **account** for a general
-module. A module that declares `own-secrets: {broker: …}` and nothing more receives thirty-two
-random bytes, not a credential. So on the broker, `emits` and `consumes` are enforced by nothing: a
-running module could bind any queue, consume any pattern, and publish under any origin, and the
-manifest that says otherwise would be describing a boundary no code draws — the exact shape of fault
-[04-ISSUES/003](../04-ISSUES/003-firewall-scope-is-read-by-no-code/00-report.md) records, a scope
-declared in manifests and read by nothing.
-
-This settles it, so a module's place on the bus is a thing the broker enforces rather than a thing
-the manifest merely claims.
-
-## Decision
-
-### A module gets a broker account when it is assigned, and its permissions are the manifest
-
-When the mesh assigns a module to a node it provisions a broker account for that module on that node,
-sealed to the node ([ADR 0004](0004-a-node-and-how-it-joins.md)) and delivered as the
-module's `own-secrets` broker — `amqps://` with the mesh's fingerprint, the shape
-[ADR 0042](0042-the-shape-of-an-event-on-the-wire.md) already carries. The account's permissions are
-derived from the manifest, and are exactly these:
-
-- **What it consumes.** Read on `mesh.events`, and configure-and-read on its own queue
- `..events` bound to the patterns in `consumes`. It cannot bind or read another
- module's queue. A module that consumes nothing gets no read on the events exchange at all.
-- **What it emits.** Write to `mesh.events`, restricted to routing keys under its own origin,
- `module..*`. It cannot publish as another module, and cannot publish under the reserved
- `mesh.*` or `node.*` origins — those belong to the mesh and the host (ADR 0042). A module that
- emits nothing gets no write.
-- **Nothing else.** The events account reaches `mesh.events` and that module's own queue, and no
- more. Tool serving and calling over `mesh.rpc` is a separate grant on the same principle — a
- module serves the tool keys it declares and calls the ones it is bound to — and is scoped the same
- way rather than folded in here.
-
-### Consuming everything is a privilege, granted deliberately
-
-`consumes: ["#"]` — the audit logger — is read across the whole bus: every module's events, the
-mesh's, every node's. That is not a pattern like any other; it is the power to see everything, and
-the account is where it becomes visible. The grant that lets one module read the entire bus is one
-the mesh issues on purpose and can be audited — the answer to *who can read everything* is a row, not
-a guess — rather than a breadth any manifest acquires by typing a single character. A `#` consume is
-a reviewed grant, not a default one.
-
-### The account is how the declaration is enforced
-
-Because the account can do only what `emits` and `consumes` name, the broker itself refuses a module
-that tries to consume a queue it did not declare or emit under an origin it does not own. That is what
-makes an event relationship a rule and not a comment — the discipline that a stated rule says how it
-is checked. A manifest that over-declares grants more than the module uses, which is visible and
-reviewable; one that under-declares makes the module fail closed at the broker, which is the safe
-direction to be wrong in.
-
-## Consequences
-
-- The control plane gains a **generic module broker-account**, derived from the manifest. The
- builder stops being a special case: its access to the build queue becomes an ordinary expression of
- what it consumes and serves, not a bespoke account method. One rule, and the builder is an instance
- of it.
-- The runtime reads its credential from a file (the broker own-secret), `amqps://` verified against
- the mesh's fingerprint. The `guest` account is for raising the substrate, never for a module — a
- module documented as holding its own credential and handed the broker's administrative one is worse
- than one with no credential story at all.
-- `emits` and `consumes` stop being advisory. They are the module's authority on the bus, so the
- manifest is now a security boundary and is reviewed as one, the discipline
- [ADR 0010](0010-delivery.md) applies to the declaration
- vocabulary.
-- *Who can read the whole bus* becomes an answerable question, because `#` is a grant and not an
- accident.
-
-## References
-
-- [ADR 0041](0041-events-are-a-relationship.md) — events are a relationship; this scopes the account
- by that relationship.
-- [ADR 0042](0042-the-shape-of-an-event-on-the-wire.md) — the wire this account secures: the queue,
- the origins, the `amqps` credential shape.
-- [ADR 0004](0004-a-node-and-how-it-joins.md) — the link is the security boundary; a
- module's account is sealed to its node the same way a node's is.
-- [ADR 0010](0010-delivery.md) — a declaration is owned
- and its additions reviewed; a module's broker permissions are that discipline applied to the bus.
-- [04-ISSUES/003](../04-ISSUES/003-firewall-scope-is-read-by-no-code/00-report.md) — a scope declared
- in manifests and enforced by no code: the fault this decision closes for events.
diff --git a/02-DECISIONS/0044-a-public-name-is-provisioned-like-any-capability.md b/02-DECISIONS/0044-a-public-name-is-provisioned-like-any-capability.md
deleted file mode 100644
index 8be68a42..00000000
--- a/02-DECISIONS/0044-a-public-name-is-provisioned-like-any-capability.md
+++ /dev/null
@@ -1,98 +0,0 @@
----
-topic: what runs on it
-status: accepted
-date: 2026-09-04
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md
----
-
-# 44. A public name is provisioned, not registered by hand
-
-> **The mechanism changed — 2026-10-06, by [ADR 0226](0226-the-private-network-is-assigned-by-its-own-name-and-the-proxy-names-its-public-issuer.md).** The interface stands; its one provider,
-> `cloudflare-dns`, left the catalogue, assigned nowhere and required by nothing. A mesh that needs a
-> public name made for it adds a provider of `public-dns` again.
-
-## Context
-
-The mesh names and resolves its own machines internally: the overlay generates
-`..` wildcards, dnsmasq answers them (`wildcard-resolution`), and the mesh
-issues a certificate for each internal name. A service reachable at a *public* domain —
-`plex.example.com`, not `plex.anchor.internal` — needs three things that machinery does not give it:
-
-- a **public DNS record** at a registrar or DNS provider, so the name resolves on the internet;
-- a **publicly-trusted certificate** for it, because the mesh's own authority is trusted by nobody
- outside the mesh;
-- and routing from that name to the module — which the reverse proxy already does: a module
- `requires` the `route` capability and the proxy provides it, routing by the host it was asked for.
-
-The routing exists. The public DNS record does not: the mesh has no way to make a name resolve on
-the public internet, so today that is a step someone does by hand at a DNS provider, outside the
-mesh, remembered nowhere. A public name is therefore the one part of reaching a service that the
-declaration graph cannot grant or withdraw — which means it is created once and outlives whatever it
-was for, the shape of drift this project exists to remove.
-
-## Decision
-
-### A public name is a capability, requested like any other
-
-A module reachable at a public host declares `requires: ["public-dns"]` and contributes the hostname
-it wants — beside `requires: ["route"]`, which exposes it through the proxy. The name is then
-provisioned on declaration ([ADR 0027](0027-a-provision-names-what-the-consumer-is-coupled-to.md)): created
-when the module is assigned, removed when it is withdrawn, reconciled like every provision.
-
-### The interface is neutral; the providers are the registrars
-
-`public-dns` is drawn at the consumer's coupling: the consumer wants *a public name that resolves to
-me*, and does not care whether Cloudflare, Route 53 or a registrar's own API puts the record there.
-So the interface is neutral and the providers are provider-scoped — `cloudflare-dns`,
-`route53-dns`, `porkbun-dns` — each implementing the one `public-dns` contract, the same way a
-neutral database coupling is answered by `postgres-database` and `mssql-database`. A module names
-`public-dns`; it never names a registrar.
-
-### The record points at the mesh's public ingress, not at the node
-
-What the name resolves to is the address the reverse proxy answers on, not the consuming machine's.
-A public service is reachable only *through* the proxy — the proxy holds the `route` grant and routes
-by host to the module — so the public name must resolve to the proxy. `public-dns` and `route` are
-the two halves of one public exposure: the name, and what the name reaches.
-
-### The record is a fact, not a secret
-
-A DNS record is public by definition, so the grant returns the fully-qualified name and its TTL and
-nothing sealed. The only secret is the provider's own API credential, which is the provider module's
-own-secret and never leaves it — the module that wanted the name never sees it.
-
-### Events
-
-The provider emits `module..record.created` and `module..record.removed`
-([ADR 0041](0041-events-are-a-relationship.md)), so *which names the mesh publishes, and where* is a
-question answered from the event trail and the grants, not from a folder of records edited at a
-provider.
-
-### The public certificate is the proxy's, and is named here only to pair it
-
-A public name without a publicly-trusted certificate is reachable and not trusted — the same pairing
-the internal name and the mesh-issued certificate already have. Obtaining that certificate (ACME
-against the now-resolving public name) is the reverse proxy's to do, and its mechanism is its own
-decision; it is named here so the pairing is not forgotten, not resolved here.
-
-## Consequences
-
-- A public name is created and torn down with the module, so it cannot outlive it, and the mesh can
- say which public names it publishes without anyone reading a registrar's dashboard.
-- Adding a registrar is adding a provider that answers `public-dns`; the modules that want names do
- not change.
-- Public exposure of a service is a trio of separate, declared, enforced relationships: the firewall
- opens the proxy's public port ([ADR 0045](0045-a-machine-firewall-is-the-sum-of-what-it-listens-on.md)),
- `route` routes the host to the module, and `public-dns` makes the host resolve.
-
-## References
-
-- [ADR 0027](0027-a-provision-names-what-the-consumer-is-coupled-to.md) — a capability is provisioned on
- declaration; a public name is one.
-- [ADR 0045](0045-a-machine-firewall-is-the-sum-of-what-it-listens-on.md) — the firewall, the other
- half of the reachability question this was asked with.
-- [ADR 0041](0041-events-are-a-relationship.md) — the provider's record events.
-- [ADR 0040](0040-what-a-module-is.md) — a provider and its interface; the neutral-interface,
- scoped-provider naming this follows.
diff --git a/02-DECISIONS/0045-a-machine-firewall-is-the-sum-of-what-it-listens-on.md b/02-DECISIONS/0045-a-machine-firewall-is-the-sum-of-what-it-listens-on.md
deleted file mode 100644
index cd85065f..00000000
--- a/02-DECISIONS/0045-a-machine-firewall-is-the-sum-of-what-it-listens-on.md
+++ /dev/null
@@ -1,98 +0,0 @@
----
-topic: what runs on it
-status: accepted
-date: 2026-09-04
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0005-the-node-host.md
----
-
-# 45. A machine's firewall is the sum of what its modules listen on
-
-> **Superseded in part — 2026-09-28, by [ADR 0138](0138-an-assignment-binds-an-endpoint-and-says-how-far-it-reaches.md).**
-> That a listen's `from` "is the whole of public-versus-internal" no longer holds: an assignment states each
-> endpoint's reach, a statement that also governs names and certificates, and `from` is the packet filter's
-> share of it. The firewall derived and applied on the node, not a provider, stands as decided here.
-
-## Context
-
-The reverse proxy is a *provider*: a module `requires` the `route` capability and a running proxy
-provides it, routing traffic by name and reaching back to the consumer. A fair question follows —
-is the firewall the same shape? Should a module *register* a port with a firewall provider the way
-it requests a route?
-
-It should not, and the difference is the point. A reverse proxy is a service another component
-performs; a firewall is a property of the machine — a packet filter the host applies to itself.
-Modelling it as a provider would invent a credential and a reach-back for something that has neither.
-
-And the mesh already has the registration: a module declares `listens: [{ port, from }]` — the port
-it accepts connections on, and from where. That *is* how a service says it wants a port open. What is
-missing is not a model but enforcement. [04-ISSUES/003](../04-ISSUES/003-firewall-scope-is-read-by-no-code/00-report.md)
-records that a `scope:` key five manifests carry is read by no code: a manifest can appear to
-restrict a port and restrict nothing — the exact fault
-[how-we-build.md](../00-META/how-we-build.md) names, *an unenforced rule is indistinguishable from a
-wrong one*, made worse because the declaration reads as a restriction.
-
-## Decision
-
-### The firewall is derived and host-applied, not a provider
-
-A machine's firewall is the sum of what the modules assigned to it declare they listen on, computed
-by the host and applied as one of its owned resources ([ADR 0005](0005-the-node-host.md):
-the host applies, it does not decide; [ADR 0010](0010-delivery.md):
-the declaration is owned resources). It is not a capability, not a per-consumer grant — opening a
-port is a declarative fact about a machine, so it is computed and applied, not requested and
-credentialed.
-
-### `from` is the whole of public-versus-internal
-
-The distinction the question is really about lives in `from`:
-
-- `listens: [{ port: 5432, from: mesh }]` — open to the private overlay only.
-- `listens: [{ port: 443, from: anywhere }]` — open to the public internet.
-
-A module registers a port on the firewall by listening on it and saying from where. There is no
-separate firewall capability, because the firewall is not a thing that reaches back or holds a
-secret; it is the machine's own filter over the ports its modules named.
-
-### The host enforces it both ways, and unknown keys are refused
-
-A port a module listens on is opened to exactly the scope it named; a port nothing declares is
-closed. And a key the firewall does not read — the `scope:` of issue 003 — is refused at the
-manifest, not accepted and ignored, so a declaration that reads as a restriction is one. This is the
-discipline [ADR 0043](0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md) applied to the
-broker account, applied here to the packet filter: the declaration is the enforcement, or it is a
-comment.
-
-### A public service is exposed through the proxy, not by opening its own port
-
-Reaching the public internet is normally not `from: anywhere` on the service's own port. The service
-listens `from: mesh` — only the proxy reaches it — and `requires: route`, so the sole machine with a
-public opening is the one running the reverse proxy, and the service is exposed by name through it.
-`from: anywhere` is the deliberate direct-exposure case, for a service that is its own front door.
-
-## Consequences
-
-- Issue 003 is closed: the firewall is computed from `listens` and enforced, so a declared scope is
- real and an undeclared port is shut. Rejecting unknown manifest keys is the general fix, of which
- the `scope:` key was one instance.
-- The firewall and the reverse proxy stop being confused for one model: the firewall is the machine's
- filter (host-derived from `listens.from`); `route` is a name-router (a provider); the public DNS
- name is a third thing ([ADR 0044](0044-a-public-name-is-provisioned-like-any-capability.md)). A
- public service uses all three.
-- The modelling question is answered: a module registers a port by declaring `listens`, and reaches
- the public internet by name through `route` + `public-dns` — never by the firewall being a
- provider.
-
-## References
-
-- [ADR 0005](0005-the-node-host.md) — the host applies; the firewall is one of
- the things it applies.
-- [ADR 0010](0010-delivery.md) — the firewall is a derived
- owned resource, not a grant.
-- [ADR 0043](0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md) — the same discipline:
- a declaration is enforced, or it is a comment.
-- [ADR 0044](0044-a-public-name-is-provisioned-like-any-capability.md) — the public name, the other
- half of the reachability question this was asked with.
-- [04-ISSUES/003](../04-ISSUES/003-firewall-scope-is-read-by-no-code/00-report.md) — the unenforced
- `scope:` this closes.
diff --git a/02-DECISIONS/0046-a-module-configuration-is-its-assignments-not-its-manifest.md b/02-DECISIONS/0046-a-module-configuration-is-its-assignments-not-its-manifest.md
deleted file mode 100644
index ffb4380c..00000000
--- a/02-DECISIONS/0046-a-module-configuration-is-its-assignments-not-its-manifest.md
+++ /dev/null
@@ -1,88 +0,0 @@
----
-topic: what runs on it
-status: accepted
-date: 2026-09-04
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md
----
-
-# 46. A module's configuration is its assignment's, not its manifest's
-
-## Context
-
-A module is assigned to a node — `assign `, always to a machine; there is no
-assignment to the mesh. "Mesh" is a *scope*, not a place: a `provides` or a `claim` scoped `mesh`
-reaches the whole mesh, but the module still runs on a node. So the two kinds of thing a module can
-carry are the manifest (what the module *is*) and, separately, what it should do *here* — which
-differs by deployment and by node.
-
-The mesh already has the second: **settings**. `settings set [--node ]` — with a node
-it is that machine's, without it the whole mesh's — layered over what the module declares and applied
-at resolution, changeable without editing the module and without a rebuild. That is the surface a
-meshboard would edit.
-
-But settings today reach only a module's **config-file content** (a mergeable file the module owns).
-Configuration that is not a file has been landing in the manifest instead, statically — a registrar's
-zone and domain, the address public names point at, and, most sharply, `listens.from`. That last one
-is the tell: whether a port is open to the private overlay or to the public internet is a
-*per-node deployment choice* — the same database internal on one machine and public on another — and
-a value fixed in the manifest is one value for every machine, so it cannot be. Static configuration in
-the manifest is configuration in the wrong place: it cannot vary per node, and it cannot change
-without a new module version.
-
-## Decision
-
-### The manifest is identity and defaults; the assignment's settings are the configuration
-
-A module's manifest declares what it is — what it provides, requires and claims, the shape of its
-resources — and, for anything configurable, a **default**. The values that make a running instance
-*this* instance are settings, carried by the assignment: per-node, or mesh-wide when no node is named,
-applied over the defaults at resolution. Change one and the next reconcile carries it; nothing is
-edited on a machine and nothing is rebuilt.
-
-### Settings drive the configurable fields the manifest marks, not only file content
-
-Settings extend beyond a config file's content to the manifest fields a module declares settable —
-foremost:
-
-- **`listens.from`**: a module declares its safe default (`from: mesh`), and a per-node setting
- raises or lowers it. postgres declares `listens: [{ port: 5432, from: mesh }]`; on the machine that
- should expose it, a setting makes that port `from: anywhere`. Same module, different exposure, and
- the firewall ([ADR 0045](0045-a-machine-firewall-is-the-sum-of-what-it-listens-on.md)) is computed
- from the effective value, so the packet filter follows the setting.
-- **A provider's own configuration**: a registrar's zone, domain and the ingress its names point at
- ([ADR 0044](0044-a-public-name-is-provisioned-like-any-capability.md)) are mesh-wide settings, not
- manifest constants — one mesh's Cloudflare zone is not another's, and the module description is the
- same for both.
-
-### Unset is the default, and an unknown setting is refused
-
-A field with no setting keeps the manifest's default, so a module runs correctly configured by nobody.
-A setting that matches no settable field — like a config value that reaches no file today — is named,
-not silently dropped, so a misspelled setting is found rather than believed (the discipline of
-`UnusedSettings`, and of [ADR 0043](0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md):
-a declaration is enforced or it is a comment).
-
-## Consequences
-
-- The postgres case works: one module, `from: mesh` by default, `from: anywhere` where a setting says
- so — internal on ace, public on novox, changeable live.
-- Provider modules stop carrying a mesh's specifics: `cloudflare-dns` describes *a Cloudflare
- registrar*, and *which* zone and ingress is a setting, so the same module serves every mesh.
-- Configuration becomes a thing a meshboard manages — set per node or mesh-wide, applied on the next
- reconcile — rather than a manifest edit and a rebuild ([ADR 0011](0011-managed-files-are-generated-never-edited.md):
- the way you change a managed thing is not by editing it).
-- What a manifest may not do is grow a value that differs per machine; if it differs per machine it is
- a setting, and the manifest holds only the default.
-
-## References
-
-- [ADR 0027](0027-a-provision-names-what-the-consumer-is-coupled-to.md) — what is provisioned on
- declaration; its per-instance values are the assignment's.
-- [ADR 0011](0011-managed-files-are-generated-never-edited.md) — a managed thing is changed through
- the mesh, not by editing it; settings are that, for configuration.
-- [ADR 0045](0045-a-machine-firewall-is-the-sum-of-what-it-listens-on.md) — the firewall follows the
- effective `listens.from`, so making `from` a setting makes exposure a setting.
-- [ADR 0044](0044-a-public-name-is-provisioned-like-any-capability.md) — the provider whose zone and
- ingress are settings, not manifest constants.
diff --git a/02-DECISIONS/0047-a-module-runs-its-code-as-its-own-process-with-its-own-account.md b/02-DECISIONS/0047-a-module-runs-its-code-as-its-own-process-with-its-own-account.md
deleted file mode 100644
index 28a5bb76..00000000
--- a/02-DECISIONS/0047-a-module-runs-its-code-as-its-own-process-with-its-own-account.md
+++ /dev/null
@@ -1,97 +0,0 @@
----
-topic: what runs on it
-status: accepted
-date: 2026-09-04
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md
----
-
-# 47. A module runs its code as its own process, with its own account
-
-> **The mechanism changed — 2026-10-02, by [ADR 0175](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md).** A module's *tools* are no longer served by the module's own process under its own account: one tool runtime per node, on the host side, serves every assigned module's bundle. A tool is still served on its own subject and only the module that serves it answers; what moved is the process and the account.
-
-## Context
-
-A module is one self-contained thing ([ADR 0040](0040-what-a-module-is.md)), and it gets a broker
-account scoped to what it emits and consumes ([ADR 0043](0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md)).
-The catalogue now gives modules **tools** and **events** — real code, in the module ([ADR 0039](0039-what-the-sdk-holds-and-refuses.md)) —
-but nothing has said what *runs* that code. The audit-logger showed one shape and was treated as an
-exception: a container running the tool runtime carrying the module's compiled code, holding the
-module's own scoped account. Every module with tools or events needs the same, and the tempting
-alternative does not work.
-
-**A node-wide runtime that loaded every assigned module's code cannot hold a per-module account.** It
-would run under one account with the union of every module's permissions — able to emit as any of
-them and read any of their queues — which is exactly the isolation ADR 0043 exists to draw. So the
-runtime is per-module, not per-node, and treating the audit-logger as special left the other
-modules' code with nothing to run it: the conversion produced tools and events that, as it stands,
-never execute.
-
-> **The hosting form is settled elsewhere — 2026-09-30.** Where this record says "a container", read
-> [ADR 0150](0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md): a module's own
-> code runs as supervised processes under this record's one account. Nothing else here changes — the
-> per-module runtime, the per-tool key and the single scoped account are the argument this record made
-> and they are why 0150 goes the way it does. The note is here because two design documents chose the
-> other form without knowing this record existed
-> ([issue 117](../04-ISSUES/117-a-modules-own-code-is-a-container-and-a-process/00-report.md)).
-
-## Decision
-
-### A module with tools or events runs a process of its own
-
-A module that has tools or events runs a **runtime process** — a container, the tool runtime carrying
-that module's compiled code — assigned and started like the module it is, holding the single broker
-account the mesh scoped to it (ADR 0043). One module, one process, one account.
-
-### It serves its tools, each on its own key
-
-A tool is served on its own key (`serve.`), and a caller invokes a named tool. Only the module
-that serves it answers, and the module's account is scoped to exactly its tool keys — so one module
-cannot answer another's calls, the isolation ADR 0043 gives events extended to tools. This supersedes
-a single `tools.invoke` endpoint that dispatched by name: that shape assumed one runtime for the
-whole node, and per-module runtimes competing on one key would each be handed calls for tools they do
-not have.
-
-### It runs its events in the same process, under the same account
-
-Emitting under the module's own origin and consuming its own queue ([ADR 0042](0042-the-shape-of-an-event-on-the-wire.md))
-happen in that same process, with that same account — not a second one to scope and seal. A module's
-tool code, its event code and, for a provider, its provisioner are the one module's code and run as
-the one module's process.
-
-### The runtime image is the tool runtime plus the module's code
-
-Built from the module's source like any module image — the audit-logger's shape, made the rule, not
-the exception. The module declares a `container` for it carrying `MESH_BROKER_FILE` (its sealed
-credential, ADR 0043) and its compiled code. A module with **neither** tools nor events runs no such
-process: a plain service module — the plex *server*, dnsmasq the resolver — is its service and files
-and nothing more. A module that is both a service and code declares both containers: the service, and
-the runtime beside it.
-
-## Consequences
-
-- The catalogue's tools and events become runnable: each tools-or-events module gains a runtime
- container with its scoped credential, and the audit-logger stops being special. Until this, the
- converted modules held code with nothing to execute it.
-- A process, and a small image, per tools-or-events module. That is the cost of ADR 0043's isolation:
- one account per module means one process per module. It is paid deliberately — a shared runtime is
- cheaper and cannot be scoped, and a mesh where any module can emit as any other is not one worth the
- saving.
-- `serve.` per key replaces the single `tools.invoke` dispatch. The sdk's serving and a module's
- account scope both come to name tools individually.
-- **A provider's provisioner is a runtime process too.** It already runs as its own container; its
- events (`bucket.created`, `database.provisioned`) belong to *that* process and need the same
- credential. So a provisioner that emits carries `MESH_BROKER_FILE` and its scoped account like any
- runtime — or it does not emit. (This is the fix for provisioners that emit today with no broker
- bound: the emit is a runtime's, and the provisioner is a runtime.)
-
-## References
-
-- [ADR 0040](0040-what-a-module-is.md) — a module is one self-contained thing; its code runs as one
- process.
-- [ADR 0043](0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md) — the scoped account
- this process holds, and the isolation that makes it per-module.
-- [ADR 0042](0042-the-shape-of-an-event-on-the-wire.md) — the events this process runs, and the
- `serve.` queue tools now use.
-- [ADR 0039](0039-what-the-sdk-holds-and-refuses.md) — the code lives in the module; this runs it.
diff --git a/02-DECISIONS/0048-a-provider-creates-the-credential-the-mesh-minted.md b/02-DECISIONS/0048-a-provider-creates-the-credential-the-mesh-minted.md
deleted file mode 100644
index e047950f..00000000
--- a/02-DECISIONS/0048-a-provider-creates-the-credential-the-mesh-minted.md
+++ /dev/null
@@ -1,131 +0,0 @@
----
-topic: what runs on it
-status: accepted
-date: 2026-09-05
-deciders: jochen
-reconstructed: false
----
-
-# 48. A provider creates the credential the mesh minted, and seals nothing
-
-## Context
-
-A provider module stands up a per-consumer resource — a database, a cache bucket, an object
-store user — and the consumer must end up holding a credential that authenticates against it.
-Building the module runtime (ADR 0047: a module runs its own code as its own process under its
-own account), the provider's provisioner was run for the first time as a delivered thing, and
-it did not work. It reads a seal key from the environment that nothing sets, and it seals every
-credential it produces to that key with a symmetric passphrase.
-
-Tracing the credential's path turned up something larger than a missing key. **The provisioner
-harness the whole catalogue is built on describes a credential flow the mesh does not have, and
-duplicates — incorrectly — one it does.**
-
-What the sdk's `runProvisioner` does today:
-
-- reads request files named `*.grant.json` — which nothing in the mesh writes;
-- calls an adapter whose `create` **generates its own password** and returns it;
-- seals that password with a symmetric key (`$MESH_SEAL_KEY`) and writes a `*.credential`
- file — which nothing in the mesh reads, and no consumer ever unseals.
-
-What the mesh already does, and has wired end to end:
-
-- The control plane mints one password per (consumer, provider) pair (`Inventory.SecretFor` →
- `secrets.Make`) and seals it to **both** node keys asymmetrically — a copy the consumer's
- host can open and a copy the provider's host can open. No shared symmetric key exists
- anywhere, on purpose: a key both ends hold is a key the mesh would have to distribute, which
- is the same problem one level down, and the control plane's own code refuses it.
-- The provider is handed, at the path its `receives` names, one contribution per consumer:
- the **login to create** (`As`, derived by the mesh so the two ends agree by construction),
- the consumer's address and requested values, and a **`Secret` file** holding that consumer's
- password sealed to the provider and unsealed onto the machine by its host.
-- The consumer is handed the *same* password, as plaintext its own host wrote by unsealing its
- copy and substituting it into a config file. The consumer never unseals anything itself and
- holds no key.
-
-So the password a provider's provisioner invents is not even the password the consumer was
-given: a consumer authenticating with the mesh's password against a resource the provisioner
-created with its own would simply fail. The symmetric seal is not an incomplete feature to
-finish delivering a key for. It is a second, contradictory credential model bolted beside the
-real one, and it cannot be made to work without building the very thing the mesh was designed
-not to have.
-
-This is a decision and not a patch because the harness is the **provider contract**. Every
-provider — the four that exist and the many a real mesh grows — is built on
-`runProvisioner(resource, adapter)`. Whatever it says a provider is, they all inherit; and
-changing it later is one migration per provider. It is cheaper and more honest to settle what
-a provider is now.
-
-## Decision
-
-**A provider is handed the credential; it does not make one, does not seal one, and does not
-hand one back.** The provisioner's only job is to make the mesh's grants true in its own
-software.
-
-Concretely, for the sdk harness and the adapter contract:
-
-- The harness reconciles the **contributions the mesh delivers** to the provider's `receives`
- path — the list of consumers, each with its login name (`As`), address, requested values,
- and the path to its unsealed password (`Secret`). It does not read `*.grant.json` and it
- does not write `*.credential`.
-- For each consumer present, the harness reads the password from that consumer's `Secret` file
- and calls the adapter to bring the resource into being under the given login. For each
- consumer no longer present — the mesh drops it from the contributions file when its consumer
- goes away — the harness calls the adapter to withdraw it.
-- The adapter shrinks to the per-software half and nothing else. It is given the login, the
- password, and the values, and it makes the resource exist or removes it. It generates no
- password, derives no name, seals nothing, and returns no credential:
- roughly `create({ as, password, values })` and `remove({ as })`, both returning nothing.
-- `$MESH_SEAL_KEY`, the symmetric `seal()`/`writeSealedCredential` path, and the `*.grant.json`
- / `*.credential` files are removed from the provisioning path entirely. The credential
- reaches the consumer through the mesh's own asymmetric channel, which already crosses node
- boundaries and holds no shared secret.
-
-Identity stays the mesh's to say. The login the provider creates is the name the mesh derived
-and gave the consumer to present; the provider never invents a name, because a name the
-consumer cannot learn is a name it cannot authenticate with.
-
-## Consequences
-
-- A provider module becomes smaller and unable to be wrong in this way: with no password to
- generate and no key to seal to, the class of bug where the two ends hold different secrets
- cannot be written. A provider added after this inherits the corrected contract and has no
- seal to reintroduce.
-- The four current providers (redis, postgres, minio, umami) each lose their `generatePassword`
- + seal code and gain a `create` that takes the password it is given. Their teardown becomes
- "withdraw the login named `As`".
-- The symmetric `seal()`/`unseal()` primitive loses its only caller and leaves — checked, not
- assumed: nothing else in the sdk or the catalogue called it, so it is removed with the
- provisioner it belonged to.
-- **How this is verified:** redis is assigned as a provider in the lab, the contributions and
- the unsealed password the mesh would deliver are put in its `receives` path, and a client
- authenticates as that consumer with the mesh's password and gets PONG — where a provider that
- invented its own password answers WRONGPASS — with `$MESH_SEAL_KEY` set nowhere and no
- `.credential` file written. Proven: `provider-uses-mesh-credential` is green.
-
-**What this does not cover — credential provisions, not data provisions.** This decision is about a
-provision whose credential is a *secret the mesh mints* — a login and password (redis, postgres,
-minio). A provider that instead *generates* the thing the consumer needs, and that thing is not a
-secret — umami's `analytics`, where the consumer wants back a `siteId` umami assigned — does not fit,
-because a contract that returns nothing has no way to hand that data back. The seal-key fault was
-never umami's (it sealed no password; it returned a public id), so removing the seal does not break
-it further, and it still reconciles its sites off the mesh's contributions. But delivering
-provider-generated data back to a consumer is a *return path* the mesh does not have and this
-decision does not build — a separate shape, left to a separate decision.
-- Teardown beyond "remove the login" — data an object store leaves behind when a consumer
- leaves — is named by each provider's adapter, not by the harness, and is out of scope here
- except to say the contract must leave room for it.
-
-## References
-
-- [04-ISSUES/032](../04-ISSUES/032-provider-runtime-has-no-seal-key/00-report.md) — the
- observation and the cross-repo trace this decision rests on.
-- ADR 0047 (the module runtime) — what first ran a provider's provisioner as a delivered
- process and exposed this; link to be filled when 0047 lands on the trunk.
-- Control-plane mechanisms this relies on already existing: `mesh-control` —
- `internal/inventory/secrets.go` (`SecretFor`, `SecretsFrom`), `internal/secrets/seal.go`
- (`Make`, the two-blob asymmetric sealing), `cmd/mesh-control/plan.go` (`grantsFor`, the
- `Grant.Sealed = ForProvider` delivery), `internal/catalogue/declaration.go` (the `receives`
- contribution: `As`, `At`, `Values`, `Secret`).
-- The path being removed: `mesh-sdk` — `src/provisioner/index.ts` (`runProvisioner`, `sealKey`,
- `writeSealedCredential`) and the symmetric `src/primitives/index.ts` `seal()`/`unseal()`.
diff --git a/02-DECISIONS/0049-a-consumers-identity-fits-the-tightest-backend.md b/02-DECISIONS/0049-a-consumers-identity-fits-the-tightest-backend.md
deleted file mode 100644
index de92a897..00000000
--- a/02-DECISIONS/0049-a-consumers-identity-fits-the-tightest-backend.md
+++ /dev/null
@@ -1,138 +0,0 @@
----
-topic: what runs on it
-status: accepted
-date: 2026-09-05
-deciders: jochen
-reconstructed: false
----
-
-# 49. A consumer's identity is bounded by the tightest backend that must accept it
-
-## Context
-
-The mesh says who a consumer is, once, and hands the same name to the provider (to create) and the
-consumer (to present), so the two ends agree by construction rather than by two conventions (the
-principle behind `ConsumerIdentity`, 04-ISSUES/023). The name is `mesh__`, cleaned to
-lower-case letters, digits and underscore.
-
-Proving the provider contract per backend (ADR 0048) turned up 04-ISSUES/034: redis and postgres
-create that name verbatim, but **minio refuses it** — an S3 access key is capped at 20 characters,
-and `mesh_anchor_bucketuser` is 22. The provisioner then retries for ever, per consumer, and the
-consumer holding that same too-long name could never present it either.
-
-Two things about the existing derivation decide most of this:
-
-- **The charset is already right.** `[^a-z0-9_]` is deliberately conservative, and its own comment
- says it reaches "a PostgreSQL role, a MinIO access key, an LDAP uid and a Keycloak client without
- quoting." That much is true.
-- **The length is wrong.** `CheckIdentity` refuses names over `identityLimit = 63`, commented as
- "the shortest identifier limit among the systems these names reach: PostgreSQL's". It is not the
- shortest — S3's 20 is shorter — so the guard that was meant to catch exactly this lets it through,
- and the failure lands at provision time as a silent retry instead of at assignment as a refusal.
-
-So this is a small wrong constant with a real cost attached: whatever bound we set, `mesh_` (5) plus
-a node name plus `_` plus a module name has to fit inside it.
-
-## The options
-
-**A — Bound the identity by the true minimum, and refuse early.** Lower `identityLimit` to the real
-shortest (20, S3's), so `CheckIdentity` refuses an over-long name *at assignment* with a clear
-message, the way it already refuses over-63 names. The derivation does not change; long names are
-simply rejected before anything is provisioned.
-- *For:* smallest change; keeps "the mesh says the identity once, verbatim" intact; the failure
- moves from a per-consumer provision-time retry to an up-front, legible refusal — which is what
- `CheckIdentity` exists to do.
-- *Against:* a hard budget. `mesh_` + node + `_` + module ≤ 20 means node + module ≤ 14 characters.
- `anchor` + `bucketuser` (16) is already over. It pushes the constraint onto how machines and
- modules are named, which is a real limitation on legible names.
-
-**B — Keep the readable name when it fits, compact it when it does not.** Below the bound, the name
-is `mesh__` as today; over it, the mesh substitutes a deterministic short form (e.g.
-`mesh_` + a truncated hash of node+module) — still one derivation, so both ends still agree.
-- *For:* no naming constraint; short backends always satisfied; the common case stays legible.
-- *Against:* some identities become opaque, and a provisioner tracing "whose login is this" loses
- the answer for exactly the consumers that overflowed. The mesh now owns a fallback format and its
- collision properties (a truncated hash is not free of collisions at 15 characters).
-
-**C — Let each interface declare its identifier bounds, and derive within the tightest a consumer
-reaches.** `s3-bucket` states `identifier: { max: 20 }`; `postgres-database` states 63; the mesh
-derives a name that fits the **minimum** bound across the providers a given consumer is granted.
-- *For:* the most precise — each provision gets exactly the room it has, and a database consumer
- keeps long legible names while an S3 consumer gets a short one; the constraint lives where the
- fact does (on the interface).
-- *Against:* the most work, and a consumer of two interfaces with different bounds must satisfy the
- smaller — so its name shortens for both, reintroducing B's opacity in a narrower case. It also
- means one consumer can hold **different** identities per provision, which the "said once" model
- currently forbids.
-
-**D — Let the provider generate a backend-valid identity and hand it back (rejected).** minio mints
-its own access key and returns it to the consumer. This is the data-provision return path this era
-keeps meeting — but it directly contradicts 023 and ADR 0048: the identity would no longer be the
-mesh's single derivation the two ends share, it would be a value one side invents and the other must
-be told. Listed for completeness; not recommended.
-
-**E — A module (and a node) may declare a short slug; the identity is built from it.** The identity
-becomes `mesh__`: where a slug is declared it is used,
-otherwise the cleaned name. A slug is a deliberately short, operator-chosen identifier — `kc` for
-keycloak, `wkstn` for a workstation. It is optional: short names (`anchor`, `redis`) need none.
-- *For:* this is the escape hatch B wanted to be, without the opacity. The name stays legible — a
- provisioner can read `mesh_wkstn_kc` and know who is asking — because a person chose it, not a
- hash function. And it makes an early refusal *palatable*: if even the slug-built identity overflows,
- the refusal points at the slug, a field made for exactly this, rather than at the machine's name.
- Both ends still derive it from one declared thing, so they agree by construction.
-- *Against:* a new optional manifest field, and someone must pick the slug — but only for names that
- would otherwise overflow, and picking a short legible identifier is a better job than being handed
- a hash.
-
-## What implementing A revealed
-
-A was tried first. At `identityLimit = 20`, the readable budget is `mesh_` (5) + node + `_` + module
-≤ 20, i.e. **node + module ≤ 14 characters** — far tighter than it looked. The catalogue's own
-existing tests use `workstation`+`keycloak` (25), which compacts to `mesh_dbbc02f8dde34d3`; common
-mesh names (`home-server`, `the-build-node`, `workstation`) blow the budget with any module. So B's
-compact fallback would fire for the *common* case, not the rare overflow — which inverts A+B: most
-identities would be opaque hashes. A alone (hard refusal at 20) would refuse most realistic names.
-This is what moved the recommendation to E: the problem is not the limit, it is that the *readable
-name* is the wrong source when it is long, and a slug is a better source than either a hash or a ban.
-
-## Recommendation
-
-**E, over a per-consumer bound (start with the global minimum, 20).** Build the identity from an
-optional slug, keep it when it fits, and refuse at assignment with "declare or shorten ``'s
-slug" when it does not — no hash, no lost legibility, and the fix is a first-class field. Set the
-bound to the true minimum (20) now; it needs no per-interface machinery to unblock S3, and a module
-that consumes S3 simply declares a short slug. Graduate to **C** (per-interface bounds) later if it
-turns out that non-S3 consumers are paying for S3's limit often enough to mind — E and C compose:
-slugs are the mechanism, per-interface bounds refine where the ceiling sits. **B is dropped**: a
-declared slug is a strictly better escape hatch than an opaque hash. **D stays rejected.**
-
-## Consequences (of E)
-
-> **The mechanism changed — 2026-10-06, by [ADR 0225](0225-a-consumers-identity-is-bounded-by-the-provision-it-requires.md).**
-> Option C, named above as the later refinement, is taken: each offer states the longest identity its
-> backend keeps, and a consumer is bounded by the provision it requires rather than by 20 everywhere.
-> 20 stays the bound of the object store and of a provider that is told its consumers and does not
-> say. The overflow is refused before merge by the catalogue check, and at composition the consumer
-> is left out of its provider's grants and reported — never the provider's machine refused. What
-> stands: the identity is said once, the slug is the remedy, nothing is hashed or truncated.
-
-- A module manifest gains an optional `slug`; a node may carry one too. `ConsumerIdentity` prefers
- the slug over the cleaned name for each half. `identityLimit` becomes 20 (the true minimum), and
- `CheckIdentity` refuses at `module add` / assignment — now with a message naming the slug to set.
-- The common case stays legible; only names that overflow the budget need a slug, and what they get
- is a name a person chose, not a hash.
-- Existing modules/nodes whose names overflow declare a slug once — a migration cost paid as a clear
- refusal with an obvious remedy, not a silent hash or a silent truncation.
-- minio (04-ISSUES/034) is unblocked: an S3 consumer declares a short slug and its access key fits.
-- **How it is checked:** the minio grant e2e — a consumer whose (slugged) identity fits reaches its
- bucket with the credential the mesh delivered — plus unit tests that a slug is preferred, that an
- un-sluggable over-long identity is refused (naming the slug), and that two consumers never collide.
-
-## References
-
-- [04-ISSUES/034](../04-ISSUES/034-mesh-login-exceeds-s3-access-key-limit/00-report.md) — the
- observation.
-- ADR 0048 — a provider creates the credential the mesh minted; the identity it creates it under is
- the one this decision bounds.
-- `mesh-control` `internal/catalogue/identity.go` — `ConsumerIdentity`, `identityUnusable`,
- `identityLimit`, `CheckIdentity` — where the constant and the check live.
diff --git a/02-DECISIONS/0050-model-access-is-vendor-agnostic.md b/02-DECISIONS/0050-model-access-is-vendor-agnostic.md
deleted file mode 100644
index b20cf54f..00000000
--- a/02-DECISIONS/0050-model-access-is-vendor-agnostic.md
+++ /dev/null
@@ -1,217 +0,0 @@
----
-topic: what runs on it
-status: accepted
-date: 2026-09-05
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0024-model-access-is-a-provision.md
----
-
-# 50. Model access is vendor-agnostic, and a vendor is an adapter
-
-## Context
-
-[ADR 0024](0024-model-access-is-a-provision.md) settled that model access is a provision and that
-a licence is a named thing an operator uses. What shipped, and runs, is a single vendor: the mesh's
-"claude" feature. A read-only trace of that feature (2026-09-05, in the code workspace) was made to
-answer whether the model-access provision is Anthropic-shaped or genuinely general. The finding is
-that **the vendor-agnostic layer already largely exists**, and the Anthropic specifics are a thin
-band around it that a per-vendor adapter can hold.
-
-**What is already general, with evidence.** `mesh-control internal/licences` models
-`licence(name, vendor, serves)` and `licence_holder(licence, node, module, sealed)`, and each
-holder's credential is sealed per-holder through `internal/secrets`. `serves` carries the
-non-secret facts (a base URL, a model) and is not vendor-specific. The `accept` verb
-([ADR 0024](0024-model-access-is-a-provision.md), and
-[`14-model-access`](../03-DESIGN/01-to-be/14-model-access.md)) already takes an operator-supplied
-value, seals it to each holder and discards the plaintext. A model the mesh runs itself answers
-`model-access` at node scope with no licence at all. None of that mentions Anthropic.
-
-**What is Anthropic-specific.** The credential is not a static key: it is a subscription OAuth grant
-— an hourly access token plus a refresh token. That shape drags four things behind it that a static
-key does not need: **central rotation** (one manager node refreshes under a lease and publishes the
-new token), **delivery that strips the refresh token** so a consuming node holds only an access token,
-an **identity guard** that reads the credential to catch a mis-binding, and a **usage** reading with
-Anthropic's own `utilization%` semantics. Most vendors are a single static key, which the sealed-key
-model already handles and which needs none of these four.
-
-**The tension at the centre of this.** A refreshable credential cannot be both *sealed so the mesh
-cannot read it* and *rotated centrally*. Central rotation means some node in the mesh holds the
-refresh token in readable form, because that is what refreshing requires. Per-holder sealing means no
-node but the holder can read the credential. For a static key the two never meet — there is nothing to
-rotate. For a refreshable grant they collide directly, and this record exists to say which gives way,
-and by how much.
-
-## Considered Options
-
-1. **Keep Anthropic special-cased in the core.** Leave the three binding columns and the `claude_*`
- schema, and add other vendors beside them the same way. **Rejected.** It is exactly what
- [ADR 0024](0024-model-access-is-a-provision.md) ruled against: a module that names a vendor cannot
- be moved onto another model without editing it, and moving it is the point. It also grows the core
- by one band per vendor, when the bands are the same shape.
-
-2. **One provision, and refuse to hold any refresh token — re-seal only.** Make every credential
- purely sealed per-holder, including refreshable ones; let each holder refresh its own grant.
- **Rejected.** It throws away the hard half [ADR 0024](0024-model-access-is-a-provision.md) says
- already works — the lease, the single-refresher, the switch-on-exhaustion — and replaces it with N
- nodes each holding a refresh token, which is the very thing today's delivery strips on the stated
- ground that *a node never holds a refresh token*. A refresh token is the long-lived secret; spraying
- it across every holder is strictly worse than keeping one copy on one node.
-
-3. **One provision, and abandon central rotation entirely** for refreshable vendors — treat the grant
- as opaque and let it expire. **Rejected.** For a subscription-seat vendor an expired access token is
- a dead licence; without rotation the feature that works today stops working. This is option 2's cost
- without option 2's autonomy.
-
-4. **One vendor-blind provision, plus a per-vendor adapter, with a bounded carve-out for the
- refreshable case.** **Adopted**, below.
-
-## Decision
-
-**`model-access` is one consumer-facing, vendor-blind provision.** A consumer declares
-`requires: model-access`, and is coupled to *reaching a model* — a base URL, a model name, a key —
-and not to which vendor answers. That is the coupling the name is drawn at
-([ADR 0040](0040-what-a-module-is.md)'s rule: name the interface at the widest boundary across which
-the consumer does not care which implementation serves it). Where a consumer were genuinely coupled to
-a specific wire API it could not swap across, the same rule would split the name — but the consumers
-that exist reach their model through a CLI or SDK that hides the vendor, so `model-access` is the true
-coupling and stays one name. This extends [ADR 0024](0024-model-access-is-a-provision.md) and
-[ADR 0027](0027-a-provision-names-what-the-consumer-is-coupled-to.md) without changing them.
-
-**A vendor is an adapter, keyed by the licence's `vendor` field.** The lifecycle a licence needs is
-vendor-specific and lives in a per-vendor adapter selected by `licence.vendor`, exactly as
-`public-dns` is one neutral interface answered by registrar-scoped providers —
-`cloudflare-dns`, `route53-dns` ([ADR 0044](0044-a-public-name-is-provisioned-like-any-capability.md)).
-A consumer names `model-access` and never a vendor, the same way a module names `public-dns` and never
-a registrar.
-
-**The field is named `vendor`, not `provider`.** The inventory already uses "provider" for the
-provider-pin — *which node answers a brokered provision*. Reusing it for *which company sells this
-licence* would collide two unrelated facts on one word. `vendor` is the licence's, and is separate.
-
-### The adapter's capabilities, all but one optional
-
-An adapter declares:
-
-- **`shape`** — `static-key` or `refreshable-grant`. This is the switch the carve-out below turns on.
-- **`accept(value) → sealed`** — take an operator-supplied credential and seal it to the holders, the
- `accept` verb [ADR 0024](0024-model-access-is-a-provision.md) already defines.
-- **`refresh(licence)`** — refreshable-grant only: the lease / rotate / publish machinery.
-- **`identity(credential) → account-id`** — the mis-binding guard, for a vendor whose credential
- carries an identity worth checking.
-- **`usage(licence) → normalised rows`** — the vendor's usage reading, mapped to the common shape below.
-- **`deliver`** — the credential *value* only; the destination path is the consumer's, not the
- adapter's.
-
-**A static-key vendor implements almost nothing** — `shape: static-key`, `accept` is the generic
-seal, `deliver` is the value, and `refresh`, `identity` and `usage` are absent or trivial. The
-abstraction earns its keep by making the common vendor small, not the rare one clever.
-
-### The carve-out — the one place the guarantee is relaxed, said plainly
-
-The mesh's standing principle is that it cannot read what it stores: `accept` seals to the holders and
-discards the plaintext ([ADR 0024](0024-model-access-is-a-provision.md)), and a provider seals nothing
-because the credential travels the mesh's own asymmetric channel
-([ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md)). A `refreshable-grant`
-credential cannot honour that principle and be centrally rotated at the same time, and central rotation
-is the working half [ADR 0024](0024-model-access-is-a-provision.md) is explicit about keeping.
-
-**So, for `refreshable-grant` vendors only:**
-
-- the **manager node holds the refresh token encrypted at rest** — readable by that node, because
- rotation requires it. This is the bounded exception.
-- **access tokens are still sealed per-holder**, as every credential is; a holder reads its own and no
- other node reads it.
-- the **refresh token is stripped on delivery** — it never reaches a consuming node. *A node never
- holds a refresh token* stays true for every node but the one manager.
-
-**Static-key vendors keep the full guarantee.** There is no token to rotate, so there is nothing to
-hold readably, so `accept` discards the plaintext and the carve-out never fires. The majority of
-vendors are static-key, and the majority therefore lose nothing.
-
-The exception is stated rather than hidden because a relaxed guarantee that is not written down is
-indistinguishable from a broken one. It is bounded on three axes at once: **refreshable-grant vendors
-only, the refresh token only, the manager node only.**
-
-### The settled details this record also fixes
-
-- **Usage is normalised to `(licence, consumer, period, metric, value)` plus the raw response as
- jsonb.** The metric is vendor-defined — Anthropic's `utilization%` is one metric, a token count is
- another — and no common unit is forced across vendors. The raw response is kept so a reading can be
- re-derived if the normalisation is later found wrong.
-- **Binding is explicit per consumer, and an unchosen consumer is refused — no implicit fallback.**
- This is the direction the resolver already takes, and it is the safe one: a mesh with several ways
- to reach a model refuses a consumer that has not said which, naming the candidates and the command,
- rather than silently choosing one ([ADR 0024](0024-model-access-is-a-provision.md), and
- [`14-model-access`](../03-DESIGN/01-to-be/14-model-access.md)).
-- **Subscription-seat authentication lives entirely inside the adapter**, never in the generic core.
- So does an interactive `/login` — an adapter-specific "adopt" origin for a credential a person must
- produce in a browser; the generic `licence key ` covers the static-key case.
-- **Anthropic is the first `refreshable-grant` adapter**, carrying the OAuth refresh, the usage
- reading, the identity guard and access-token-only delivery. **`anthropic-api-key` is a
- `static-key` adapter for the same vendor's plain API keys**, and is the early second case that
- proves the abstraction is not a single vendor wearing a coat: it exercises the whole path with the
- carve-out switched off.
-
-## Consequences
-
-- **The carve-out is the mesh's one deliberate relaxation of "it cannot read what it stores."** It is
- bounded to refreshable-grant vendors, to the refresh token, and to the manager node; the static-key
- majority keep the full guarantee unchanged. This is the open risk the analysis carried here, and it
- is recorded as an exception rather than pretended away.
-- **Anthropic collapses from special case to adapter.** The three binding columns
- (`nodes.node_license`, `nodes.hal_claude_account`, `agents.claude_account`) become three ordinary
- consumers of `model-access`; the `claude_*` schema becomes the generic licence tables plus one
- adapter. What was hardcoded becomes data keyed by `vendor`.
-- **Adding a vendor is adding an adapter, and a static-key vendor is nearly free.** The modules that
- want a model do not change when a vendor is added — they named `model-access`, not a vendor.
-- **The refresh-token concentration is now a stated property to defend, not an accident.** The manager
- node is a place a long-lived secret lives readably, and losing it or compromising it is a bounded,
- named blast radius rather than a surprise.
-
-### How each claim here is checked
-
-- **Vendor-blind provision, static-key path.** A lab scenario binds an `anthropic-api-key` licence to
- a consumer; the consumer resolves, receives a key sealed to its node, and reaches a model — and the
- key is **nowhere in the control plane's database** nor in anything that crossed the broker. This is
- the `licence_holder` sealed-per-node check that [`14-model-access`](../03-DESIGN/01-to-be/14-model-access.md)
- already runs, now asserted for a second vendor.
-- **The carve-out is exactly as narrow as stated.** For a `refreshable-grant` licence, a test asserts
- the refresh token exists (encrypted) **only on the manager node**, is **absent from every holder's
- delivery**, and that the delivered credential is access-token-only — and that for a `static-key`
- licence no refresh token is stored anywhere.
-- **Adapter selection is keyed by `vendor`.** A scenario with two vendors on two licences verifies each
- licence's lifecycle runs its own adapter, and that a consumer naming `model-access` never names a
- vendor to get one.
-- **Refuse-if-unchosen.** Already checked in [`14-model-access`](../03-DESIGN/01-to-be/14-model-access.md):
- a consumer with more than one candidate is refused with the candidates and the command named.
-
-## References
-
-- [ADR 0024](0024-model-access-is-a-provision.md) — model access is a provision, a licence is a named
- thing, and `accept`; this record generalises its single vendor and keeps its working central
- rotation.
-- [ADR 0027](0027-a-provision-names-what-the-consumer-is-coupled-to.md) — a provision names the
- coupling; `model-access` is drawn at the consumer's.
-- [ADR 0040](0040-what-a-module-is.md) — the naming rule and the neutral-interface / scoped-provider
- shape a vendor adapter follows.
-- [ADR 0044](0044-a-public-name-is-provisioned-like-any-capability.md) — registrar-scoped `public-dns`
- providers, the precedent a `vendor`-scoped adapter mirrors.
-- [ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md) — the mesh seals credentials
- and holds no readable copy; the carve-out here is the bounded, named exception to that for a
- refreshable grant.
-- [`03-DESIGN/01-to-be/14-model-access.md`](../03-DESIGN/01-to-be/14-model-access.md) — the design this
- record extends, amended to describe the adapter generalisation.
-- The read-only vendor-agnostic analysis, 2026-09-05 (code workspace) — the inventory and the decisions
- taken on the open questions this record encodes.
-
-> **The mechanism changed — 2026-10-02, by [ADR 0183](0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md).**
-> What stands: `model-access` is one vendor-blind provision for the consumers that do not care which
-> vendor answers; a vendor's lifecycle is an adapter's; the carve-out that one node holds a refreshable
-> grant's refresh token readably. What moved: the Anthropic adapter is no longer a part of the
-> controller's licences context but a module, `claude-licence-manager`, holding the seat
-> `anthropic-licence-manager`, with the grants in its own store encrypted with a key the vault made for
-> it; and the agent at a terminal is not a consumer of `model-access` — it is coupled to an Anthropic
-> grant and uses the seat. The consequence above that the three binding columns *become three ordinary
-> consumers of `model-access`* therefore no longer describes the agent's bindings; they are the
-> manager's. The static-key adapters and the vendor-blind records stay where this record put them.
diff --git a/02-DECISIONS/0051-shared-data-is-the-operators.md b/02-DECISIONS/0051-shared-data-is-the-operators.md
deleted file mode 100644
index f48954ba..00000000
--- a/02-DECISIONS/0051-shared-data-is-the-operators.md
+++ /dev/null
@@ -1,166 +0,0 @@
----
-topic: what runs on it
-status: accepted
-date: 2026-09-05
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0030-data-outlives-the-mesh-that-declared-it.md
----
-
-# 51. Shared data is the operator's, and a module is granted access to it
-
-## Context
-
-**Eight modules declared one filesystem as eight private ones.** The media stack — a library
-server, the acquisition managers for films, series, music and books, a subtitle fetcher and two
-download clients — shares directories on one machine: the download clients write into
-`/services/media/downloads` and the managers read it; the managers write into the libraries and
-the library server reads them. That sharing is the entire point of the stack. Yet each module
-declared every shared directory it touched as its own `directory` resource, with an owner and a
-mode. `/services/media/downloads` was written seven times, as seven private directories that
-happen to be the same path.
-
-**The resolver refuses exactly that, and is right to.** Two modules declaring one path on one node
-are refused by name, with no exemption for identical content and no merge — because two owners of
-one path is the class of fault this repository keeps recording ([04-ISSUES/036](../04-ISSUES/036-six-modules-own-what-they-must-share/00-report.md)).
-So the stack as written refuses its own only sensible assignment: all of it on one machine,
-sharing one filesystem. It passed today only because no test co-resolves any two of the eight. The
-first machine assigned two of them together is where the refusal would have surfaced.
-
-**The vocabulary had one word for two intentions, and this was already seen.**
-[04-ISSUES/026](../04-ISSUES/026-the-data-directories-are-not-declared/00-report.md) found the
-same gap from the other side and named it precisely: two kinds of mount are spelled identically —
-*the directory my data lives in*, which the mesh creates and owns, and *a facility I was granted*,
-which already exists and the mesh only reaches. That issue deferred inventing a field to tell them
-apart, because doing so is a design decision and it declined to make one to get a check green. This
-is that decision.
-
-**A `directory` resource is owned, on every axis.** The host creates it, sets its owner and mode,
-and removes it when it is empty and no longer declared — it *removes what it made and leaves what
-it merely configured* ([ADR 0005](0005-the-node-host.md), [ADR 0030](0030-data-outlives-the-mesh-that-declared-it.md)).
-A shared media library is none of that. It existed before the mesh, several modules read and write
-it at once, and losing it is the one failure that does not recover. It is not any module's
-resource; it is the operator's, and a module only needs to be let at it.
-
-## Considered Options
-
-1. **Make the stack one module with several containers**, the way the mail system already is. The
- issue raises it directly: is a set of modules that must share a filesystem really one module?
- **Rejected.** The eight are independently assignable and independently useful — a person may run
- the download client without the library server, or the film manager without the music one — and
- folding them into a single module to express a shared directory would make *what a module is*
- turn on an incidental filesystem contract. It also does not generalise: the next pipeline of
- modules handing files to each other on one machine (an ingest folder, a spool, a drop directory)
- would face the same wall and the same wrong remedy.
-
-2. **One module owns the directories and the rest `require` them.** **Rejected**, and this is the
- heart of the decision. Nobody owns shared operator data. The library predates the mesh and
- outlives any one module, so making the library server or a manager its owner means unassigning
- that module orphans everyone else's access — and the owner would set the owner and mode of a
- tree it did not create. Ownership is the wrong relationship to model, because the true owner is
- not a module at all.
-
-3. **A flag on a directory resource** — `external: true`, or an owner of `operator`. **Rejected.**
- It overloads one shape with a boolean that inverts every one of its semantics: created becomes
- *must already exist*, owned becomes *touch nothing*, removed-when-empty becomes *never removed*.
- That is the *two-kinds-spelled-identically* trap of 04-ISSUES/026 reintroduced with a single
- quiet field — a reviewer reading `type: directory` would have to check one boolean elsewhere to
- know whether the mesh owns the thing at all.
-
-4. **A distinct `accesses` declaration, separate from resources.** **Adopted.**
-
-## Decision
-
-**Shared, pre-existing data is operator-owned and external. The mesh does not create it, does not
-set its owner or mode, does not reconcile it and does not remove it.** A media library, a download
-spool, an ingest directory is the operator's, and the mesh is a guest in it.
-
-**A module declares that it needs *access* to such a path, not that it owns a resource there.** The
-manifest field is `accesses`: a list of `{path, mode}`, where mode is `read` or `read-write` and
-absent narrows to `read` — the safe default, because the danger with an access is being given more
-than was meant, not less. A module's own configuration and state directories stay owned
-`directory` resources; only the shared, pre-existing paths become accesses.
-
-**The host mounts an accessed path and owns nothing about it.** It reaches the machine as a new
-declaration shape, `access`, distinct from `directory`. The host confirms the path is present and
-does nothing else — no create, no chown, no mode, no removal.
-
-**An accessed path absent at apply time is refused, clearly, not created.** The mesh does not own
-it, so conjuring it would be a lie the host then acts on — and specifically the lie 04-ISSUES/026
-records, where a bind mount whose source does not exist is made by the container runtime as root
-with the wrong ownership. The host says the operator must provide the path instead.
-
-**Several modules accessing one path is normal, and never refused.** The duplicate-path refusal is
-about *ownership*, not *use*: it applies to resources a module owns and to those alone. An access
-is not a resource and never enters the check, so the eight-module stack co-resolves. What stays
-refused is genuine rivalry — two modules owning one path — and the new contradiction it exposes: a
-path one module owns while another merely accesses it, because that asserts both that the mesh owns
-the directory and that the operator does.
-
-This is a decision and not a patch because it settles *what a module may say about a path it did
-not make*, which every co-located file-handoff in the catalogue now and later depends on — and
-because it draws the ownership line [ADR 0030](0030-data-outlives-the-mesh-that-declared-it.md)
-started: the host owns what it made and keeps what it merely configured, and this adds the third
-case it did not have a word for — what it neither made nor configured, and must not touch.
-
-### How each claim is checked
-
-- **The stack co-resolves.** A control-plane unit test assigns two modules that declare access to
- one path on one node and asserts no refusal — the exact case the resolver refuses when the same
- path is owned. The mirror test, two modules *owning* one path, still refuses, so the sharing
- vocabulary does not weaken the rule it sits beside.
-- **Ownership and access cannot both be claimed of one path.** A unit test asserts the resolver
- refuses a path one module owns and another accesses, naming both.
-- **Absent is refused, not created.** A host unit test applies an access to a path that does not
- exist and asserts a clear refusal that names the operator, and that nothing was created.
-- **Present is confirmed and nothing moves.** A host unit test applies an access to an existing
- directory and asserts the apply reports no change and disturbs nothing.
-- **Undeclaring never removes.** A host unit test drops a previously declared access and asserts
- the operator's directory and its contents are left exactly as they were — the data-loss failure
- [ADR 0030](0030-data-outlives-the-mesh-that-declared-it.md) exists to prevent, on a directory the
- mesh never made.
-- **Every media manifest is corrected.** No `/services/media/*` path is an owned `directory`
- resource in any of the eight; each is an `accesses` entry, and each module's own config and state
- directories remain owned. Checked by the control-plane manifest parser, which now understands
- `accesses` and refuses a malformed one.
-
-## Consequences
-
-**A shared filesystem between co-located modules now has a vocabulary**, and it is not the media
-stack's alone: any pipeline handing files to a neighbour on one machine — an ingest directory, a
-spool, a drop folder — says *I access this operator path* rather than *I own this directory*, and
-several of them may say it of one path.
-
-**Unassigning a module that reached shared data leaves the data.** Correct, and the same trade
-[ADR 0030](0030-data-outlives-the-mesh-that-declared-it.md) made for owned directories: removing
-data is a person's act, done knowingly, not a side effect of unassignment.
-
-**The operator must provision the shared paths before the stack is applied**, and a machine that
-lacks one is told plainly which. That is a real new obligation, and it is the right one: the mesh
-cannot own what predates it, so it cannot create it either, and saying so at apply time beats a
-directory conjured as root and a service that half-works.
-
-**The host vocabulary grew by one shape**, which is a cost — every added shape widens what a
-compromised control plane can express ([ADR 0005](0005-the-node-host.md)). It is a narrow one: an
-`access` is confirmed by a stat and grants the host no new action. It earns its place by letting
-the host refuse to create what it must not own, which no existing shape could say.
-
-**A path can be both owned and accessed only by refusal.** If a future manifest declares one path
-as an owned directory in one module and an access in another, the resolver refuses it rather than
-guessing which is meant — the two assertions about who owns the data cannot both hold.
-
-## References
-
-- [04-ISSUES/036](../04-ISSUES/036-six-modules-own-what-they-must-share/00-report.md) — six (in
- fact eight) modules own what they must share; the problem this resolves
-- [04-ISSUES/026](../04-ISSUES/026-the-data-directories-are-not-declared/00-report.md) — the two
- kinds of mount spelled identically, which deferred this field to a decision
-- [ADR 0030](0030-data-outlives-the-mesh-that-declared-it.md) — data outlives the mesh; the host
- keeps what it did not make. This record adds the case it had no word for
-- [ADR 0005](0005-the-node-host.md) — the host removes what it made and leaves what it merely
- configured; the vocabulary is finite and every shape is a security decision
-- [ADR 0040](0040-what-a-module-is.md) — what a module is; an access is a new thing a module may
- say about the machine it lands on
-- mesh-control `feat/shared-data-access`, mesh-catalog `feat/media-access-not-ownership`,
- mesh-host `feat/mount-operator-owned` — the mechanism, the corrected manifests, and the host
- shape
diff --git a/02-DECISIONS/0052-a-step-that-runs-once-before-a-container.md b/02-DECISIONS/0052-a-step-that-runs-once-before-a-container.md
deleted file mode 100644
index 96783bdb..00000000
--- a/02-DECISIONS/0052-a-step-that-runs-once-before-a-container.md
+++ /dev/null
@@ -1,198 +0,0 @@
----
-topic: what runs on it
-status: accepted
-date: 2026-09-05
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0005-the-node-host.md
----
-
-# 52. An init step is a container run once to completion, gating what follows
-
-## Context
-
-**A module can declare things that exist; it cannot declare a step that runs.** The host owns a
-finite vocabulary of shapes — `file`, `directory`, `service`, `package`, `container`, `action` —
-and every one but `action` describes *state*: a thing that should be present, with content or a
-mode or an image, which the host reconciles toward ([ADR 0005](0005-the-node-host.md)). That is
-right for what it covers. But a real class of modules needs, once, to *run their own code at a
-point in their own lifecycle* — and the vocabulary has no word for it
-([04-ISSUES/037](../04-ISSUES/037-a-module-cannot-run-code-at-a-lifecycle-phase/00-report.md)).
-
-**mosquitto is the sharp case, and it fails silently without this.** Its Dynamic Security plugin
-loads at broker start and refuses to come up unless `dynamic-security.json` already holds an admin
-client. Seeding that file is a step that must happen *after* the data directory exists and *before*
-the broker container starts. The manifest can declare the directory, the config file and the broker
-container; it cannot declare "seed this, once, before that container starts." Written as it is
-today, the broker starts against an unseeded store and the plugin aborts — and the next reconcile
-does not fix it, because nothing in the declaration ever seeds the file.
-
-**It is not one module's defect.** The database providers need the same to run a first-boot
-migration, an extension enable, or a health gate before they are announced ready; today that works
-only where the *image* happens to seed itself from an environment variable, and anything the mesh
-must run once against the server has no home. This is the timing face of the same gap
-[04-ISSUES/035](../04-ISSUES/035-reconciling-a-seed-file-wipes-what-grew-in-it/00-report.md) records
-from the content side: the manifest needed *the file to exist before first start*, and had only
-*the file has this content, forever*.
-
-**The obvious answer is the one that already went wrong.** An earlier mesh had exactly this as a
-feature — event-driven hooks that ran custom code at phases of build, publish and deploy. It was
-powerful and it was *complex to set up and flaky*, and that fragility, not the need, is the content
-of the issue. Whatever this becomes must not rebuild that engine.
-
-**The ground has shifted since that engine, in a way that makes a much smaller answer possible.** A
-module with tools or events now runs a **process of its own** — a container carrying the module's
-compiled code, holding the single broker account the mesh scoped to it, isolated from every other
-module ([ADR 0047](0047-a-module-runs-its-code-as-its-own-process-with-its-own-account.md)). The
-code that must run at first boot is *already in that image, under that account*. So the mesh does
-not need a way to run a module's code — it has one. It needs a way to say **run this container to
-completion, and start the one that depends on it only after it has.**
-
-## Considered Options
-
-1. **A host `run` shape — a command the host executes on the machine.** The direct reading of
- "run code at a lifecycle phase." **Rejected.** The host's vocabulary is finite and every added
- shape is a security decision, because it widens what a *compromised control plane* can express
- ([ADR 0005](0005-the-node-host.md)). A general "run this command as the host" is the largest
- such widening there is: the blast radius is the whole machine, as root. The mesh already drew
- this exact line for `action` — it runs a command, and so it is *permitted from the bundle and
- refused from the link*, because the bundle arrives with the binary and the link is a separate
- party with an unbounded reach. A new host-command shape usable by an ordinary module would be an
- `action` from the link by another name, which is precisely what is refused.
-
-2. **Per-phase lifecycle hooks on a module** — `pre-start`, `post-start`, `pre-remove`, and their
- build/publish cousins, each naming code the mesh runs at that phase. The general answer, and the
- old feature. **Rejected for now.** It is the flaky engine the issue warns against, and most of
- its phases have no present need. Deciding the full set of phases, where each one's code runs, and
- how each is made idempotent is a large design taken to buy capability nothing yet asks for. The
- three blocked modules all need one phase — *before a container starts* — and a mechanism narrow
- enough to be obviously correct beats a general one that is not.
-
-3. **A distinct one-shot resource type** — a new shape, sibling to `container`, that names an image
- and runs it once. **Rejected.** It grows the host vocabulary by a whole shape (a `Type`, a
- struct, an applier, a place in every host's shape list) to express something a `container` almost
- already is. A one-shot *is* a container — a pinned image, an account, volumes, an environment —
- that happens to exit. Spending a new shape on the difference is the cost of option 1 in smaller
- type, for a capability the existing shape can carry with one modifier.
-
-4. **A modifier on the existing `container` shape: this container runs once, to completion, and the
- host gates the apply on it.** **Adopted.** It reuses the shape the host already has, adds no new
- host action, and leans on two guarantees the host already gives — *apply in declared order,
- never sorted*, and *a failed step fails the apply* — to turn "before that container starts" into
- an emergent property of ordering rather than a dependency graph the host must resolve.
-
-## Decision
-
-**A run-once step is an ordinary `container`, marked to run to completion.** The manifest sets
-`run-once: true` on a container resource. Everything else about it is a container as before — a
-digest-pinned image ([ADR 0006](0006-the-substrate-and-the-control-plane.md)), volumes, an
-environment, and for a module's own code the same scoped account its runtime already holds
-([ADR 0047](0047-a-module-runs-its-code-as-its-own-process-with-its-own-account.md)). The mesh adds
-no way to run code; it marks a container the host must **run to completion and require to exit 0**,
-rather than start and leave running.
-
-**The gate is declaration order, not a named dependency.** The host applies a declaration in the
-order it is given, does not sort, and does not resolve dependencies — ordering is a decision, and it
-is the control plane's ([ADR 0005](0005-the-node-host.md),
-[04-ISSUES/013](../04-ISSUES/013-a-file-arrives-after-the-service-that-needs-it/00-report.md)). A
-run-once step is placed *before* the container that depends on it, and **a failed run-once step
-halts the apply**, exactly as a failed `action` does — so everything the declaration places after
-it, the broker included, is never reached until the step has completed. "Before the broker starts"
-is therefore expressed by list position plus completion, and the host cross-references nothing.
-
-**Completion is recorded, and a re-apply does not re-run it.** The host records what it applied only
-after the fact, as the digest of the declaration that produced it
-([ADR 0018](0018-a-picture-is-read-from-what-runs.md)) — a run-once step no differently. Because the
-step leaves nothing running to inspect, that persisted digest, not a live container, is the marker
-that it happened. On a later apply the host finds the digest already recorded for this exact
-declaration and does nothing; it re-runs only when the declaration's digest has changed, and a step
-that exited non-zero recorded nothing and so is retried next apply. This is the reconcilable,
-idempotent discipline the state shapes get for free, made explicit for a step.
-
-This is a decision and not a patch because it settles **what a module may say about running its own
-code**, which the whole catalogue of providers — a seed before start, a first-boot migration, a
-health gate — now and later depends on, and because it draws the line the issue asked for: the
-narrowest sound mechanism that unblocks the three modules without rebuilding the hook engine whose
-fragility is the warning.
-
-### The security bound, stated plainly
-
-**The host gains no new action and no new shape.** `run-once` is a boolean modifier on the
-`container` shape that already exists. A run-once container is strictly *less* powerful than an
-`action`: it cannot run an arbitrary host command, only a digest-pinned image under an account the
-mesh scoped — which is exactly the capability `container` already grants from the link. A control
-plane that is compromised can express nothing through `run-once` it could not already express by
-declaring an ordinary `container`. The dangerous expansion of option 1 — a command the host runs on
-the machine — is not made.
-
-### How each claim is checked
-
-- **A run-once step runs to completion and its exit 0 is required.** A host unit test applies a
- run-once container whose image exits 0, asserts the host ran it to completion (not detached, not
- left running) and reported it done; a sibling test applies one that exits non-zero and asserts
- the apply fails, naming the step.
-- **A failed run-once step gates what follows.** A host unit test places a run-once container that
- exits non-zero before another container and asserts the second is never started and the apply is
- reported gated — the mirror of the existing test that a failed action stops what follows.
-- **It is not re-run once it has completed.** A host unit test applies a run-once step, then applies
- the identical declaration again with the first run's record present, and asserts the second apply
- runs nothing and reports the step unchanged.
-- **A changed declaration re-runs it.** A host unit test applies a run-once step, then applies one
- whose image or environment differs, and asserts it runs again — the digest moved, so the marker no
- longer matches.
-- **The vocabulary carries the field end to end.** A control-plane unit test resolves a module whose
- manifest marks a container `run-once` and asserts the rendered host declaration carries the field,
- in author order before the container it gates; the manifest parser refuses a `run-once` that is
- not boolean.
-- **mosquitto seeds before the broker.** mosquitto's manifest declares a run-once init container,
- before the `server` (broker) container, that writes the admin client into `dynamic-security.json`
- and exits — checked by the control-plane resolver, which now understands the field, and by the
- ordering of the rendered declaration. The end-to-end proof that it runs exactly once, at the right
- phase, and converges on re-apply is owed to a lab scenario ([04-ISSUES/037] open question), which
- this record does not close.
-
-## Consequences
-
-- **The three blocked modules gain a home for their step.** mosquitto seeds its dynsec admin before
- the broker; a provider that must migrate or health-gate at first boot declares a run-once step in
- its own runtime image, under its own account, before the container that depends on it.
-- **The general lifecycle hook is deferred, deliberately.** Only *before a container starts* is
- bought here. `post-start`, `pre-remove` and the build/publish phases remain unbuilt, and the day
- one is genuinely needed it is decided then, against a need, not speculatively — the same restraint
- that kept this from being the old engine.
-- **The seed-then-mutate file is safe if the step is written to be.** A run-once seed writes
- `dynamic-security.json` only when it is absent and never reconciles it, so what the running plugin
- grows in that file afterward is never wiped
- ([04-ISSUES/035](../04-ISSUES/035-reconciling-a-seed-file-wipes-what-grew-in-it/00-report.md)).
- The host's marker guarantees the step is not re-run; the step's own code guarantees it does not
- clobber on the pass it does run.
-- **The host vocabulary did not grow, and that is the point.** The cost of a run-once step is one
- boolean and a completion path in the container applier, not a new shape and not a new action. The
- mesh expresses ordering and completion; the module runs its own code, where it already runs it.
-- **A run-once step that never converges is a stuck apply, loudly.** A step that exits non-zero
- every time halts the apply every time, and the container it gates never starts — which is the
- correct failure, reported, rather than a broker that half-starts against an unseeded store and a
- reconcile that reports success. It is failed forward, not failed silent.
-
-## References
-
-- [04-ISSUES/037](../04-ISSUES/037-a-module-cannot-run-code-at-a-lifecycle-phase/00-report.md) — a
- module cannot run its own code at a lifecycle phase; the gap this resolves, and the warning about
- the old hook engine
-- [04-ISSUES/035](../04-ISSUES/035-reconciling-a-seed-file-wipes-what-grew-in-it/00-report.md) — the
- content face of the same gap: a file needed before first start, that the running program then
- mutates
-- [04-ISSUES/013](../04-ISSUES/013-a-file-arrives-after-the-service-that-needs-it/00-report.md) — the
- order of a declaration is the control plane's, and the host applies it as given; the gate rests on
- this
-- [ADR 0005](0005-the-node-host.md) — the host's finite vocabulary, the ordered declaration it does
- not sort, and `action` as the shape a command already is and why it is refused from the link
-- [ADR 0047](0047-a-module-runs-its-code-as-its-own-process-with-its-own-account.md) — a module runs
- its code as its own process under its own account; a run-once step is that process, run to
- completion
-- [ADR 0018](0018-a-picture-is-read-from-what-runs.md) — what was applied is recorded after it works;
- the completion marker is that record
-- [ADR 0006](0006-the-substrate-and-the-control-plane.md) — a container is pinned by digest; a
- run-once container no differently
-- mesh-control `feat/lifecycle-run-once`, mesh-host `feat/apply-run-once`, mesh-catalog
- `feat/mosquitto-bootstrap` — the vocabulary, the apply support, and mosquitto's seeded broker
diff --git a/02-DECISIONS/0053-a-step-that-runs-on-a-schedule.md b/02-DECISIONS/0053-a-step-that-runs-on-a-schedule.md
deleted file mode 100644
index 865ce7a2..00000000
--- a/02-DECISIONS/0053-a-step-that-runs-on-a-schedule.md
+++ /dev/null
@@ -1,181 +0,0 @@
----
-topic: what runs on it
-status: accepted
-date: 2026-09-06
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0052-a-step-that-runs-once-before-a-container.md
----
-
-# 53. A scheduled step is a container run on a recurring schedule
-
-## Context
-
-**[ADR 0052](0052-a-step-that-runs-once-before-a-container.md) gave the mesh a step that runs *once*;
-a real class of modules needs one that runs *again and again*.** kometa reconciles a media library
-against its lists on a timer; a ticketing integration polls its source for new work every few minutes;
-a backup, a cache warm, a metrics roll-up all recur. The host's vocabulary describes *state* — a file,
-a directory, a container that should be running — and 0052 added *a step that happens once and is
-done*. Neither says *this should happen every night at 3, forever*
-([04-ISSUES/037](../04-ISSUES/037-a-module-cannot-run-code-at-a-lifecycle-phase/00-report.md) named
-the lifecycle gap; 0052 closed the once-before-start face of it and left the recurring face open).
-
-**The modules that need it already run their own code.** As with run-once, the ground has shifted
-since the old mesh's flaky hooks: a module with tools or events runs a **process of its own** — a
-container carrying the module's compiled code under the single scoped account the mesh gave it
-([ADR 0047](0047-a-module-runs-its-code-as-its-own-process-with-its-own-account.md)). The work that
-must recur is already in that image, under that account. The mesh does not need a way to run a
-module's code on a timer — it has the code and the account. It needs a way to say **run this
-container again on this cadence**.
-
-**The shape of the answer is already decided, one modifier over.** 0052 rejected a host `run` command,
-per-phase hooks, and a distinct one-shot resource type, and adopted *a modifier on the `container`
-shape the host already has*, because a one-shot is a container that happens to exit. A scheduled step
-is the same container that happens to exit — run repeatedly. Reusing that shape keeps the host
-vocabulary flat and inherits 0052's security bound whole. The only thing 0052's `run-once` does not
-carry is *when to run it again*.
-
-**One difference from run-once changes a rule, and it is the reason this is its own record.** A
-run-once step **gates the apply**: it is placed before the container that depends on it, and a failure
-halts everything after it, because "seed the store before the broker starts" is a correctness
-precondition ([ADR 0052](0052-a-step-that-runs-once-before-a-container.md)). A scheduled step is the
-opposite: it runs *after* the machine is up and converged, on its own clock, and a single failed run
-is an ordinary operational event — the next run comes anyway. A scheduled step that halted the apply,
-or that a failed run marked the node not-current over, would make a routine poll into a reason the
-whole machine reads as broken. So the gating rule 0052 established is exactly the rule this record
-must **not** inherit.
-
-## Considered Options
-
-1. **A host `cron`/`timer` shape — the host installs a system timer that runs a command.** The direct
- reading. **Rejected**, for the reason 0052 rejected a host `run` shape: it widens what a
- *compromised control plane* can express toward "run this command on the machine, forever," which is
- the largest widening there is, and it is `action`-from-the-link by another name
- ([ADR 0005](0005-the-node-host.md)). A recurring command is worse than a one-off, because it
- persists.
-
-2. **Per-phase lifecycle hooks** — `on-schedule` joining `pre-start`/`post-start` as named code the
- mesh runs. **Rejected for now**, as in 0052: it is the flaky hook engine the issue warns against,
- and the three modules that need this need one thing — *run this container on a cadence* — which a
- narrow modifier expresses without deciding a whole hook vocabulary.
-
-3. **A distinct `scheduled` resource type**, sibling to `container`. **Rejected**, as 0052 rejected a
- distinct one-shot type: it spends a whole new host shape (a `Type`, a struct, an applier, a place
- in every host's shape list) on something a `container` already almost is — a scheduled task *is* a
- container (pinned image, account, volumes, environment) that runs on a clock.
-
-4. **A modifier on the existing `container` shape: `schedule`, a cron expression the host runs the
- container on.** **Adopted.** It reuses the shape the host has, adds no new host action, and sits
- beside `run-once` as its recurring twin — the same container, exited, run again.
-
-## Decision
-
-**A scheduled step is an ordinary `container`, marked with a `schedule`.** The manifest sets
-`schedule: ""` on a container resource — a standard five-field cron expression. Everything else
-about it is a container as before: a digest-pinned image
-([ADR 0006](0006-the-substrate-and-the-control-plane.md)), volumes, an environment, and for a module's
-own code the same scoped account its runtime already holds
-([ADR 0047](0047-a-module-runs-its-code-as-its-own-process-with-its-own-account.md)). The mesh adds no
-way to run code; it marks a container the host must **run on that cadence, each time to completion**,
-rather than start once and leave running (a service) or run once and gate (a run-once step).
-
-**A run is fired by the clock, not by the apply, and does not gate it.** Applying the declaration
-installs the schedule; it does not run the step. The machine converges — reports applied and current —
-as soon as the schedule is installed, exactly as it does for a service that is running. Thereafter the
-host fires the container when the cron expression is due. This is the deliberate inversion of 0052:
-a scheduled step is downstream of convergence, not a precondition of it.
-
-**A failed run is recorded and the next run still comes; it never marks the node not-current.** A run
-that exits non-zero is logged against the module — visible, auditable — but it does not fail the apply,
-does not halt other resources, and does not flip the node's reported state. A poll that fails at 03:00
-and succeeds at 03:05 is the system working, not a machine that needs attention. A step that fails
-*every* time is a loud, repeating log entry, which is the correct signal for "this recurring job is
-broken" — distinct from "this machine did not converge."
-
-**Runs do not stack.** If a run is still going when the next is due, the host skips the due run rather
-than starting a second copy, and logs the skip. A slow nightly job that occasionally overruns must not
-spawn a growing pile of concurrent containers competing for the same account and volumes — the failure
-mode that made the old timers dangerous.
-
-**Each run is independent and idempotent by the module's own code.** The mesh guarantees only *the
-container is run on the cadence*; that a run does the right thing when the previous one half-finished
-is the module's contract, the same discipline a run-once seed owes
-([ADR 0052](0052-a-step-that-runs-once-before-a-container.md),
-[04-ISSUES/035](../04-ISSUES/035-reconciling-a-seed-file-wipes-what-grew-in-it/00-report.md)).
-
-This is a decision and not a patch because it settles **what a module may say about running its own
-code on a cadence**, which every recurring provider job — a sync, a poll, a roll-up — now and later
-depends on, and because it draws the line 0052 left: the recurring twin of run-once, with the gating
-rule deliberately reversed so a routine job's failure is never a machine's failure.
-
-### The security bound, stated plainly
-
-**The host gains no new action and no new shape.** `schedule` is a string modifier on the `container`
-shape that already exists. A scheduled container is strictly *less* powerful than an `action`: it runs
-a digest-pinned image under an account the mesh scoped, which is exactly what `container` already
-grants from the link, and it cannot run an arbitrary host command. A compromised control plane can
-express nothing through `schedule` it could not already express by declaring a `container` — the cron
-string only says *how often*, not *what*. The dangerous expansion of option 1 — a command the host
-runs on the machine on a timer — is not made.
-
-### How each claim is checked
-
-- **A scheduled step runs when the schedule is due.** A host unit test installs a container with a
- schedule that is due immediately (or advances a injected clock to when it is due) and asserts the
- host ran it to completion; a sibling test with a schedule not yet due asserts it has not run.
-- **Installing it does not run it, and the node is current without a run.** A host unit test applies a
- scheduled container and asserts the apply reports current *before* any run has fired — the schedule
- is state that is present, not a step that gated.
-- **A failed run does not fail the apply or the node.** A host unit test fires a scheduled container
- that exits non-zero and asserts the failure is recorded against the module, the apply is not failed,
- and the node stays current — the mirror of the run-once test where a non-zero exit *does* halt.
-- **Runs do not stack.** A host unit test fires a scheduled container whose run outlasts its next due
- time and asserts the host skipped the due run and logged the skip, rather than starting a second
- container.
-- **The vocabulary carries the field end to end.** A control-plane unit test resolves a module whose
- manifest sets `schedule` on a container and asserts the rendered host declaration carries the field;
- the manifest parser refuses a `schedule` that is not a valid cron expression, and refuses a container
- that is both `run-once` and `schedule` (a step is one or the other, never both).
-- **A real module recurs in the lab.** A converted module declaring a scheduled step (kometa's library
- sync, or a poller) is assigned in a lab scenario, and the scenario asserts the scheduled container
- fires on its cadence and its account and volumes are the module's — the end-to-end proof this record
- owes, as 0052 owed its run-once lab proof.
-
-## Consequences
-
-- **The recurring providers gain a home for their cadence.** kometa reconciles on its schedule; a
- poller polls; a roll-up rolls up — each a scheduled step in its own runtime image, under its own
- account, on the cron it declares.
-- **The general lifecycle hook is still deferred.** Only *run once before* (0052) and *run on a
- cadence* (this) are bought. `post-start`, `pre-remove` and the build/publish phases remain unbuilt,
- decided when a real need arrives, not speculatively — the restraint that kept both from being the old
- engine.
-- **A recurring job's failure is loud but not fatal.** The node stays current while a scheduled step
- fails and retries; a step that fails forever is a repeating log entry, not a machine marked broken.
- This is the correct separation — a machine's convergence and a job's success are different questions —
- and it is why this could not simply be `run-once` without the schedule.
-- **The host vocabulary did not grow, again, and that is the point.** The cost of a scheduled step is
- one string field and a cron loop in the container applier, not a new shape and not a new action. The
- mesh expresses cadence; the module runs its own code, where it already runs it.
-- **`run-once` and `schedule` are exclusive and complete for now.** A container runs once and gates, or
- runs on a cadence and does not, or runs and stays up (a service). A manifest that asks for two of
- these at once is refused, because the three are distinct answers to "how does this container run."
-
-## References
-
-- [ADR 0052](0052-a-step-that-runs-once-before-a-container.md) — the run-once step; this is its
- recurring twin, reusing the `container`-modifier shape and inheriting its security bound, and
- reversing its gating rule
-- [04-ISSUES/037](../04-ISSUES/037-a-module-cannot-run-code-at-a-lifecycle-phase/00-report.md) — a
- module cannot run its own code at a lifecycle phase; 0052 closed the once-before-start face, this
- closes the recurring face
-- [ADR 0047](0047-a-module-runs-its-code-as-its-own-process-with-its-own-account.md) — a module runs
- its code as its own process under its own account; a scheduled step is that process, run on a cadence
-- [ADR 0005](0005-the-node-host.md) — the host's finite vocabulary, and why a host command (even on a
- timer) is refused from the link
-- [ADR 0006](0006-the-substrate-and-the-control-plane.md) — a container is pinned by digest; a
- scheduled container no differently
-- [ADR 0018](0018-a-picture-is-read-from-what-runs.md) — what was applied is recorded after it works;
- the installed schedule is state, a fired run is an event
-- mesh-control `feat/schedule-container`, mesh-host `feat/apply-schedule`, and the converted module
- that first declares a scheduled step — the vocabulary, the apply support, and the recurring proof
diff --git a/02-DECISIONS/0054-model-usage-is-recorded-at-two-grains.md b/02-DECISIONS/0054-model-usage-is-recorded-at-two-grains.md
deleted file mode 100644
index 47336f84..00000000
--- a/02-DECISIONS/0054-model-usage-is-recorded-at-two-grains.md
+++ /dev/null
@@ -1,178 +0,0 @@
----
-topic: what runs on it
-status: accepted
-date: 2026-09-06
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0050-model-access-is-vendor-agnostic.md
----
-
-# 54. Model usage is a vendor-neutral record, produced by the adapter, at two grains
-
-## Context
-
-**[ADR 0050](0050-model-access-is-vendor-agnostic.md) fixed the *shape* of a usage reading and left
-its *home* open.** It decided that an adapter may expose `usage(licence) → normalised rows`, that a
-row is `(licence, consumer, period, metric, value)` plus the raw vendor response as jsonb, and that the
-metric is vendor-defined (Anthropic's `utilization%` is one metric, a token count another). It did not
-say where those rows are stored, how they get produced on a cadence, or whether the only grain is the
-whole licence — and the mesh's first real consumer needs all three answered.
-
-**The predecessor mesh recorded usage at two grains, and both are wanted.** It polled the vendor for a
-licence-level reading (an account's `utilization%`), and it also attributed **per-session** token and
-cost — which model, how many input and output tokens, what it cost — parsed from the agent's own
-transcript, including the case where a long session switched the account it billed against mid-way. The
-licence-level reading answers "how close is this subscription to its cap"; the session-level reading
-answers "what did this piece of work cost, and against which account." A mesh that kept only the first
-could not bill a project or notice a runaway session; keeping only the second could not see a cap
-approaching. Both are load-bearing and neither subsumes the other.
-
-**A session is already a consumer, so the second grain needs no second vocabulary.**
-[ADR 0026](0026-the-mesh-has-a-session-of-its-own.md) and design 15 establish that an agent session is
-a thing in its own right, and that **model access is bound to the session, not to the machine** — a
-session is a *consumer* of `model-access`, identified by `(node, module)` and its own id. That is
-exactly the `consumer` column ADR 0050 already put in the usage row. So the two grains are not two
-schemas; they are the same row at two consumer resolutions: the holding module for the licence grain,
-the session for the finer one. The design's own still-open worker-naming gap (design 14) is the same
-gap here and is left where it is — a session id distinguishes what `(node, module)` cannot.
-
-**The pieces this needs already exist.** A periodic reading is a **scheduled step**
-([ADR 0053](0053-a-step-that-runs-on-a-schedule.md)) — the adapter's `usage()` poll is a container the
-mesh runs on a cadence, which is precisely what 0053 was built for. A durable audit of what happened is
-**an event the audit trail records** ([ADR 0041](0041-events-are-a-relationship.md),
-[ADR 0042](0042-the-shape-of-an-event-on-the-wire.md)); the `audit-logger` module already consumes every
-event. And a queryable current picture is **a context store**, the same shape the licences themselves
-live in ([ADR 0008](0008-a-context-owns-its-store.md)). Nothing new in kind is required; what is missing is
-the decision to point them at usage.
-
-## Considered Options
-
-1. **Licence grain only — a single `utilization%` poll, nothing per session.** Rejected: it cannot
- attribute cost to a piece of work or catch a session that is burning an account down, which is half
- of why usage is recorded at all.
-
-2. **A bespoke `sessions` schema mirroring the old mesh's `*_sessions` / `*_session_account_usage`
- tables.** Rejected: it reintroduces a second, vendor-shaped vocabulary for something the mesh
- already names — a session is a consumer, and its usage is a usage row. A parallel schema would drift
- from the `model-access` vocabulary and force every reader to learn two.
-
-3. **Store usage only as raw vendor blobs, normalise later.** Rejected as the *whole* answer (kept as a
- fallback within the chosen one): a reader that must parse Anthropic's response shape to answer "what
- did this cost" has the vendor coupling the whole feature exists to remove. The raw blob is kept
- beside the normalised row (0050 already requires this), not instead of it.
-
-4. **One vendor-neutral usage record at two consumer grains, produced by the adapter, recorded as
- both an event and a queryable row.** Adopted.
-
-## Decision
-
-**Model usage is one vendor-neutral record — `(licence, consumer, period, metric, value)` plus the raw
-response — recorded at two grains that differ only in the `consumer`.** At the **licence grain** the
-consumer is the holding module and the metric is the vendor's own account reading (Anthropic:
-`utilization%`). At the **session grain** the consumer is the agent session — `(node, module)` and its
-session id ([ADR 0026](0026-the-mesh-has-a-session-of-its-own.md)) — and the metrics are the ones a
-session bills: input tokens, output tokens, model, and cost. The row shape is 0050's, unchanged; the
-grain is which consumer the row is *for*.
-
-**The adapter is the only thing that knows the vendor, and it produces both grains.** Reading an
-account's cap is the adapter's `usage(licence)` verb ([ADR 0050](0050-model-access-is-vendor-agnostic.md));
-attributing a session's cost is the adapter reading that vendor's transcript or usage API and emitting
-rows keyed to the session. The mesh defines the row and the plumbing; the adapter fills it from whatever
-the vendor exposes, and a static-key vendor that exposes nothing simply produces no rows — usage is an
-optional reading, not a requirement of holding a licence.
-
-**A reading is taken on a schedule, not on a request.** The licence-grain poll is a **scheduled
-container** ([ADR 0053](0053-a-step-that-runs-on-a-schedule.md)) the adapter runs on a cadence; the
-session-grain rows are produced as sessions progress, from the transcript the session already writes.
-Neither blocks anything: a poll that fails is a logged, retried scheduled run (0053's rule), and a
-session whose cost cannot yet be attributed is a row not yet written, never a session refused.
-
-**Usage is recorded two ways, for two audiences.** Each reading is **emitted as an event**
-([ADR 0041](0041-events-are-a-relationship.md)) — an immutable "this was observed at this time" that the
-`audit-logger` already records, so the history of what an account did is in the audit trail by default,
-under nobody's special arrangement. And the **current** picture — the latest reading per
-`(licence, consumer, period, metric)` — is upserted into a **usage context store**, so "how close is
-this cap" and "what has this project spent this month" are a query, not a fold over the event log. The
-event is the record of what happened; the store is the answer to what is true now.
-
-**Usage is not a credential, and is recorded in the clear.** The one thing the mesh must not read is the
-sealed key ([ADR 0050](0050-model-access-is-vendor-agnostic.md), the refresh-token carve-out aside). A
-token count and a cost are not secrets; they are the operator's own operational facts, recorded openly
-so they can be queried, audited, and charged against. This is the deliberate opposite of the credential
-rule, and stating it prevents a later reader assuming usage inherits the key's secrecy and hiding it
-from the person who is paying.
-
-This is a decision and not a patch because it settles **where a model's usage lives and at what grain**,
-which every reader — a bill, a cap alarm, a per-project report — depends on, and because it closes the
-half [ADR 0050](0050-model-access-is-vendor-agnostic.md) explicitly left open, reusing the session
-([ADR 0026](0026-the-mesh-has-a-session-of-its-own.md)), the schedule
-([ADR 0053](0053-a-step-that-runs-on-a-schedule.md)), the event ([ADR 0041](0041-events-are-a-relationship.md))
-and the store ([ADR 0008](0008-a-context-owns-its-store.md)) the mesh already has rather than inventing a
-vocabulary beside them.
-
-### How each claim is checked
-
-- **A usage row is vendor-neutral and carries the raw beside it.** A unit test constructs an Anthropic
- `utilization%` reading and a token/cost reading and asserts both render to
- `(licence, consumer, period, metric, value)` with the vendor response preserved in the raw column;
- a reader that answers "what did this cost" touches only the normalised columns.
-- **The two grains differ only in the consumer.** A unit test records a licence-grain row (consumer =
- the module) and a session-grain row (consumer = a session id) for one licence and asserts both are
- the same shape and both are returned when the licence's usage is asked for, distinguishable by
- consumer.
-- **A poll is a scheduled run and its failure is not fatal.** The adapter's usage container declares a
- `schedule` ([ADR 0053](0053-a-step-that-runs-on-a-schedule.md)); a host test (0053's) already proves a
- scheduled step runs on cadence, does not gate, and logs rather than fails on a non-zero run — the poll
- inherits this and adds nothing to check.
-- **Each reading is an event the audit trail records.** An integration check asserts a usage reading
- emits an event that the `audit-logger` receives (it consumes `#`), so the history is present without
- the usage module and the audit module knowing about each other beyond the event.
-- **The current picture is a query.** A store test upserts two readings for one
- `(licence, consumer, period, metric)` and asserts the later replaces the earlier, so "what is true
- now" is one row, while the event log keeps both.
-- **A static-key vendor with no usage reading records nothing, and that is fine.** A test resolves a
- `static-key` model-access consumer whose adapter has no `usage` verb and asserts the licence works and
- no usage rows or poll are required — usage is optional, holding a licence is not conditioned on it.
-- **Usage is readable in the clear; the key is not.** A test asserts a usage row is stored unsealed and
- is returned to an ordinary query, while the licence key remains sealed and absent from the same
- surfaces — the deliberate inversion of the credential rule.
-
-## Consequences
-
-- **A bill and a cap alarm are both queries.** "What did project X spend this month" reads the
- session-grain rows; "how close is account Y to its cap" reads the latest licence-grain metric — both
- from the usage store, neither a fold over events or a call to the vendor.
-- **The session becomes the unit of cost, which is what it already is.** Because a session is the
- consumer, attributing cost needs no new identity — and the worker-granularity gap
- ([design 14](../03-DESIGN/01-to-be/14-model-access.md)) surfaces here exactly as it does for access,
- to be closed once, for both, when a session id is threaded through.
-- **The adapter carries the vendor's usage quirks alone.** Anthropic's `utilization%`, its transcript
- shape, a mid-session account switch — all live in the Anthropic adapter; the mesh, the store, and
- every reader see only rows. A second vendor adds a second adapter and no new table.
-- **Usage history is durable and tamper-evident by reuse, not by a new mechanism.** It rides the event
- trail the mesh already keeps, so an operator who wants the whole history has it, and one who wants the
- current number has the store — without usage owning either mechanism.
-- **The refresh-token carve-out is untouched by this.** Usage is read *from* an authenticated adapter;
- it neither holds nor exposes the credential, so the one place the mesh reads what it stores
- ([ADR 0050](0050-model-access-is-vendor-agnostic.md)) is not widened by recording what that credential
- was spent on.
-
-## References
-
-- [ADR 0050](0050-model-access-is-vendor-agnostic.md) — model access is vendor-agnostic; fixes the
- usage row shape and the `usage(licence)` adapter verb, and leaves its home open — which this closes
-- [ADR 0026](0026-the-mesh-has-a-session-of-its-own.md) — the mesh has a session of its own; a session
- is the consumer the finer grain attributes to
-- [ADR 0053](0053-a-step-that-runs-on-a-schedule.md) — a scheduled step; the licence-grain poll is one
-- [ADR 0041](0041-events-are-a-relationship.md) — events are a relationship; a usage reading is one, and
- the audit-logger records it
-- [ADR 0042](0042-the-shape-of-an-event-on-the-wire.md) — the shape of an event on the wire; the form a
- usage reading takes to reach the audit trail
-- [ADR 0008](0008-a-context-owns-its-store.md) — a context owns its store; the current usage picture
- lives in one
-- [ADR 0024](0024-model-access-is-a-provision.md) — model access is a provision; usage is a reading of
- what that provision was used for
-- [03-DESIGN/01-to-be/14-model-access.md](../03-DESIGN/01-to-be/14-model-access.md) — the model-access
- design; this fills its usage section and shares its open worker-naming gap
-- [03-DESIGN/01-to-be/15-the-agent-session.md](../03-DESIGN/01-to-be/15-the-agent-session.md) — the agent
- session; the consumer the session grain is keyed to
diff --git a/02-DECISIONS/0055-model-access-is-answered-by-a-licence-or-a-node.md b/02-DECISIONS/0055-model-access-is-answered-by-a-licence-or-a-node.md
deleted file mode 100644
index d148d68a..00000000
--- a/02-DECISIONS/0055-model-access-is-answered-by-a-licence-or-a-node.md
+++ /dev/null
@@ -1,146 +0,0 @@
----
-topic: what runs on it
-status: accepted
-date: 2026-09-07
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0050-model-access-is-vendor-agnostic.md
----
-
-# 55. Model access is answered by a licence, or by a node that hosts the model
-
-## Context
-
-**[ADR 0024](0024-model-access-is-a-provision.md) made model access a provision, and
-[ADR 0050](0050-model-access-is-vendor-agnostic.md) fixed what answers it: a record — a licence — that
-an adapter turns into a sealed vendor credential.** A consumer requires `model-access`, is put on a
-licence, and is delivered a key (a static API key, or an access token a manager refreshes). Every
-answer so far has been a credential to reach a vendor's API across the internet.
-
-**But a model need not come from a vendor. A node in the mesh can host one.** An operator with a GPU
-runs Ollama or vLLM, which serves an OpenAI-compatible API on that node. A consumer that wants that
-model does not need a vendor credential — it needs the model server's **endpoint**: the base URL and
-the model name, and a key only if the server is configured to want one. This is model access answered
-by a **node**, not by a record.
-
-**The mesh already knows how a node answers a provision — it is the ordinary provider/consumer path.**
-A provider `provides` a provision at a scope, `serves` its connection facts, and the mesh fills the
-consumer's bound facts with the provider's `at`/`port` and each served fact — exactly how a Postgres
-consumer learns where its database is. Nothing reserved `model-access` to records: a node offering
-`provides: ["model-access"]` resolves through this path, and the resolver already **prefers a local
-answer over a licence** — its own comment names the case, "a model the mesh runs itself." So the
-capability exists; what is missing is the decision to use it, and the statement of what a node-answer
-delivers and where it stops.
-
-**A node-answer and a record-answer are the same provision with two shapes of answer.** This is the
-same move [ADR 0050](0050-model-access-is-vendor-agnostic.md) already made for shapes within the
-vendor path (static-key vs refreshable-grant): one provision, more than one way it is answered. A
-consumer written against `model-access` should not care whether the model behind it is a vendor's or
-the mesh's own — it asks for model access and is given what reaches a model.
-
-## Considered Options
-
-1. **A separate provision for the local case (`local-model`, `model-endpoint`).** A node answers that;
- `model-access` stays record-only. Rejected: it splits "where my model comes from" into two
- provisions a consumer must choose between in its manifest, when the mesh already models a
- record-answer and a node-answer to **one** provision. A consumer would have to know, at authoring
- time, whether its model will be a vendor's or the mesh's — the exact coupling the provision was
- meant to remove. It is the safer implementation (see the limitation below) but the worse interface.
-
-2. **Model access answered by either a licence or a node, under the one provision.** A consumer
- requires `model-access`; the operator answers it with a licence (a vendor) or by assigning a
- node that hosts a model. Adopted: one interface, and the answer is an operator's deployment choice,
- not a consumer's authoring choice.
-
-## Decision
-
-**Model access is one provision answered two ways: by a licence (a record, turned into a sealed vendor
-credential by an adapter — [ADR 0050](0050-model-access-is-vendor-agnostic.md)) or by a node that hosts
-the model (a provider that serves an endpoint).** A consumer requires `model-access` and is delivered
-whichever the operator assigned; it does not name the kind.
-
-**A node-answer delivers an endpoint, not a credential.** The provider `provides: ["model-access"]`
-and `serves` its connection facts — the port it listens on and the model it runs — and the mesh fills
-the consumer's bound facts with the provider node's `at`, the served `port`, and the served `model`,
-the same way every provider consumer learns where its provider is. The consumer assembles a base URL
-(`http://:/v1`) and points an OpenAI-compatible client at it. If the local server wants a
-key, the provider mints one the ordinary way (a per-consumer secret, sealed and host-unsealed); if it
-does not — the common Ollama case — the consumer lists `model-access` under `binds` and **not** under
-`secrets`, and no key is delivered. Secret delivery and fact delivery are already independent, so a
-keyless endpoint is expressed by asking for the facts and not a secret.
-
-**A node-answer uses no adapter.** The adapter registry ([ADR 0050](0050-model-access-is-vendor-agnostic.md))
-is the vendor-credential machinery — accept-and-seal, refresh, usage. A node-hosted model has no vendor
-secret to seal; its endpoint is served, and its key (if any) is minted like any provider's. The adapter
-is consulted only for the record/vendor answer. So the vendor-agnostic decision is untouched, and the
-node-answer adds no vendor logic anywhere.
-
-**The resolver prefers a local answer.** When a node's own set answers `model-access` — a model the
-mesh runs itself — a licence for it is not consulted. This is already the resolver's behaviour and is
-made a decision here: a mesh that runs a model uses it, and a licence is the answer for a consumer that
-has no local model, not a competitor to one that does.
-
-**One limitation, stated so it is not found as a bug.** The local-preference above is exact for a
-**node-scope** provider co-located with its consumer, and for any node-answer in a mesh that holds no
-`model-access` licence. It is *not* yet exact for a **mesh-scope** model server — one node serving the
-model to others — **while a licence for `model-access` also exists in the same mesh**: the record pass
-that turns a licence into an answer keys on same-node satisfaction and would still demand the licence be
-used, double-answering. Until that pass is taught to stand down when a brokered node need already
-answers, a mesh-scope local model and a vendor licence must not both answer `model-access` in one mesh.
-A node-scope local model has no such constraint. This is named because an unstated limitation is
-indistinguishable from a bug, and costs more.
-
-This is a decision and not a patch because it settles **what may answer model access** — a question
-every model-access consumer's meaning depends on — and because it lets the mesh's own hosted models sit
-behind the same provision as the vendors', which is what makes "the mesh can run its own model" a
-deployment choice rather than a second interface to build against.
-
-### How each claim is checked
-
-- **A node answers model access without a licence, and is preferred over one.** A resolver test
- assigns a consumer and a module that `provides: ["model-access"]` at node scope on the one node, with
- a licence also present, and asserts no resolved need is answered by the record — the local model
- answers and the licence is ignored. (This test exists; the decision adopts what it proves.)
-- **The consumer is delivered an endpoint, not a credential.** A mesh bed assigns a model-server
- provider and a consumer that binds `model-access` and does not list it under `secrets`, and asserts
- the consumer's config carries `OPENAI_BASE_URL` built from the provider's served `at`/`port`, and
- that no key file was delivered to its secret path.
-- **The local endpoint is reachable through what the consumer was given.** The bed makes a request to
- the base URL the consumer wrote and asserts the model server answers — the wiring, not a model's
- output, is what is proven (the server may be a stub; a real model is not needed to prove the mesh
- routed the consumer to it).
-- **A node-answer consults no adapter.** A node-answered `model-access` need is resolved with the
- vendor registry never read — asserted by the absence of any vendor on a node-answered need and the
- ordinary served-facts delivery.
-- **The scope limitation holds where stated.** The node-scope case is what the bed and the resolver
- test exercise; the mesh-scope-plus-licence collision is recorded here and left for the resolver
- change that reconciles the two answer passes, not worked around in a module.
-
-## Consequences
-
-- **The mesh can run its own model, and a consumer reaches it through the same `model-access` it uses
- for a vendor.** One interface, two answers; a consumer moves between a vendor and a local model by an
- operator reassigning its provision, not by a code change.
-- **A local model is keyless by default and keyed by the ordinary path when it must be.** Nothing new
- is invented for the local server's credential: it either has none, or mints one the way every
- provider does.
-- **The vendor path is untouched.** Adapters, the refresh carve-out, and usage
- ([ADR 0054](0054-model-usage-is-recorded-at-two-grains.md)) are the record answer's business; a
- node-answer neither uses nor changes them. A local model that exposes usage would serve it as facts,
- not as an adapter's usage verb.
-- **The two answer passes meet in one place, and must be reconciled there.** The mesh-scope limitation
- is the single point where a node-answer and a record-answer to the one provision can collide; it is
- named, and its fix is a resolver change, not a per-module workaround.
-
-## References
-
-- [ADR 0024](0024-model-access-is-a-provision.md) — model access is a provision; this decides a node
- may answer it, not only a record
-- [ADR 0050](0050-model-access-is-vendor-agnostic.md) — model access is vendor-agnostic; the record
- answer and its adapters, which the node answer sits beside and does not use
-- [ADR 0054](0054-model-usage-is-recorded-at-two-grains.md) — model usage; a vendor's business on the
- record path, served as facts (if at all) on the node path
-- [ADR 0008](0008-a-context-owns-its-store.md) — a context owns its store; a licence is a record in one,
- a node-answer needs none
-- [03-DESIGN/01-to-be/14-model-access.md](../03-DESIGN/01-to-be/14-model-access.md) — the model-access
- design, which this extends with the node answer
diff --git a/02-DECISIONS/0066-public-routing-is-name-agnostic.md b/02-DECISIONS/0066-public-routing-is-name-agnostic.md
deleted file mode 100644
index 8c4f88e3..00000000
--- a/02-DECISIONS/0066-public-routing-is-name-agnostic.md
+++ /dev/null
@@ -1,131 +0,0 @@
----
-topic: the tiers
-status: accepted
-date: 2026-09-09
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0007-connectivity.md
----
-
-# 66. Public routing is name-agnostic, its names are resolved inside the mesh, and an internal authority can certify them
-
-> **Superseded in part — 2026-10-03, by [ADR 0191](0191-the-meshs-resolver-holds-only-the-meshs-own-names.md).**
-> One clause of the decision below no longer holds: *publishing a granted name into internal resolution,
-> mesh-wide*. A public name now resolves publicly, and only names under the mesh's own suffix get a private answer.
-> Inside the mesh a route is reached and certified by its internal name
-> ([ADR 0151](0151-a-routes-internal-name-is-composed-under-the-node-that-serves-it.md)). The label, the
-> node's public domain and their composition stand as decided here.
-
-## Context
-
-**[ADR 0007](0007-connectivity.md) and [connectivity §3](../03-DESIGN/01-to-be/08-connectivity.md)
-made a public route a grant: a workload that must be reachable requires a route, the proxy provides
-it, the consumer contributes the name it wants and the port it listens on.** What was never pinned
-is **what that name is** — and building a whole mesh in the lab showed the gap costs more than it
-looks.
-
-**The catalogue shipped each route as a full domain.** A module that needed a public name carried
-that name, in full, as a literal in its manifest. Running the same catalogue against a different
-domain — a lab standing in for production, or a second operator's mesh — meant overriding that
-literal on every routed module, per node. The mesh was, in effect, carrying a **map of names to
-services**: the one thing it should never hold, because a name is the operator's choice (one runs
-the forge at `git`, another at `code`) and the domain is the node's, and neither is the mesh's to
-know.
-
-**And a second gap surfaced the moment an internal issuer tried to certify those names.**
-[Connectivity §5](../03-DESIGN/01-to-be/08-connectivity.md) already states the issuer must be
-configurable and that the lab runs its own ACME authority. With that authority wired to the proxy,
-issuance still could not complete: the authority accepted the order and offered a challenge, then
-**could not connect to the validation target.** Nothing inside the mesh resolved the public route
-name. The mesh publishes each `.internal` name into every container, but not the public names
-the proxy serves — so a validator living in the mesh had no address to reach, and a name the mesh
-cannot resolve is a name it cannot have certified.
-
-**The two are one problem.** A name the mesh can *compose* from parts it is given, and *propagate*
-to whoever needs to resolve it, is exactly a name it can also have *certified* — and the reverse:
-without the composition and the propagation, neither the routing nor the certificate is the
-operator's to move between meshes.
-
-## Considered Options
-
-**1. Keep the full domain in the manifest, override per node.** The status quo. It works, and it is
-wrong in the specific way this repository cares about: the catalogue holds a domain map, lab and
-production differ by an override on every routed module rather than one fact, and a module manifest
-names something — the public domain — that belongs to the node, not the module. An unowned name in
-the wrong place is the shape of a leak.
-
-**2. A module declares a label; the node declares its public domain; the mesh composes.** The route
-contribution carries a subdomain the operator chose, the node carries its public domain as
-node-level configuration, and the mesh joins `.` and grants exactly that. The
-mesh interprets nothing. Lab-versus-production becomes one node setting. Chosen.
-
-**3. For certification, issue only from a publicly reachable node against a public authority.**
-This is already true for public meshes and stays true. It is not an option for a lab or an
-internal-only mesh: there is no public authority to answer, and no public reachability to validate
-against. An internal issuer is required there — and an internal issuer must be able to *validate*,
-which it cannot do unless the routed name resolves and is reachable **inside** the mesh. So the
-resolution gap is not optional to close; it is what makes an internal authority possible at all.
-
-## Decision
-
-**The mesh core holds no map of hostnames, subdomains or domains.** A route contribution carries a
-**label** (the subdomain) chosen by the module's operator. A node contributes its **public domain**
-as node-level configuration. The mesh composes `.`, grants exactly that name,
-and never interprets what it means. One operator's forge at `git.example.tld` and another's at
-`code.other.example` are the same module with two facts supplied around it.
-
-**When the proxy is granted a name, the mesh publishes that name → the node that serves it into
-internal resolution, mesh-wide** — the same mechanism, and the same "given by the mesh, not chosen
-by a module," that already writes `.internal` into every declared container
-([connectivity §2](../03-DESIGN/01-to-be/08-connectivity.md)). The mesh propagates the names it was
-told to serve. It still knows nothing about what any of them mean.
-
-**An internal authority certifies those names by the same path a public one would.** The proxy is
-pointed at whichever issuer the mesh names — a public ACME authority, or an internal one — and
-trusts that issuer's root; nothing else about issuance changes. The internal authority validates by
-reaching the routed name, which the clause above has just made resolvable inside the mesh. So the
-three are one decision: **compose the name, propagate it, certify it** — each is meaningless without
-the one before it.
-
-> **The mechanism changed — 2026-09-30, by [ADR 0148](0148-the-meshs-names-are-resolved-not-copied-into-containers.md).**
-> A routed name still reaches every asker in the mesh, which is what this record decided and it stands.
-> It no longer reaches them by being written into each declared container: copying the roster in made the
-> roster part of every container's identity, so one name moving replaced every container in the mesh
-> ([issue 151](../04-ISSUES/151-a-new-name-recreates-every-container-in-the-mesh/00-report.md)). A
-> container resolves through its machine's resolver instead. The consequence below — that an internal
-> issuer's challenge needs the routed name resolvable inside the mesh — holds unchanged, by the means the
-> machine itself already uses.
-
-## Consequences
-
-- **Lab-versus-production is one node-level `public-domain` setting**, not an override on every
- routed module. The same catalogue runs against any domain.
-- **The manifest layer needs composition it does not yet have.** Today a route name is stored as a
- literal, with no interpolation of a node's domain into a module's label. Until that exists, the
- composed name is produced by a per-node settings override — a stopgap that reproduces option 1's
- per-module cost and is explicitly *not* the design.
-- **An internal issuer depends on route-name resolution.** Its challenge validates against the
- routed name; without that name in internal resolution, issuance for it cannot complete inside the
- mesh. The lab found this as a live failure, not a theory.
-- **Nothing about the public path changes.** A publicly reachable node issuing a public name from a
- public authority is untouched; this widens the same shape to names and meshes that are not public.
-
-**How each is checked** — an unenforced rule is indistinguishable from a wrong one:
-
-- **Name-agnostic:** the same catalogue resolves against two different public domains by changing
- one node setting and nothing else; and no module manifest contains a full public domain. A
- manifest that pins an FQDN is the smell the check looks for.
-- **Resolution:** a request to a routed name, made from inside the mesh, reaches the workload that
- serves it — and, the sharper check, issuance for that name against the internal authority
- completes, which it cannot unless the validator resolved and reached the target.
-- **Internal authority:** a TLS handshake to a routed name verifies against the internal root and
- nothing else, the same shape §5 already uses for internal node-to-node names.
-
-## References
-
-- [ADR 0007 — connectivity](0007-connectivity.md), which made a route a grant and named exposure,
- resolution and certificates as one context.
-- [ADR 0009 — modules and the graph](0009-modules-and-the-graph.md), the provide/require/contribute
- vocabulary a route and an authority both use.
-- [Connectivity design §2, §3, §5](../03-DESIGN/01-to-be/08-connectivity.md), amended alongside this
- record.
diff --git a/02-DECISIONS/0067-genesis-is-a-pivot.md b/02-DECISIONS/0067-genesis-is-a-pivot.md
deleted file mode 100644
index 060a7056..00000000
--- a/02-DECISIONS/0067-genesis-is-a-pivot.md
+++ /dev/null
@@ -1,129 +0,0 @@
----
-topic: the tiers
-status: accepted
-date: 2026-09-10
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0006-the-substrate-and-the-control-plane.md
----
-
-# 67. Genesis is a pivot: a temporary control plane installs the registry that makes it permanent
-
-## Context
-
-**The mesh builds its own modules into its own registry, and there is no public registry for them
-— that is the point, not an omission.** A module is cloned from the forge, built, and published
-where the mesh can move, replace and back it up. Nothing about that arrangement wants a copy of the
-mesh's code hosted by somebody else.
-
-**Every image must be pinned by digest** ([ADR 0006](0006-the-substrate-and-the-control-plane.md)),
-and the reasoning is exactly right: a bundle is applied where no mesh exists to check anything
-against anything, so what it names must be exact.
-
-Those two sentences are individually correct and together they close a door. The digest a pin means
-is a *manifest* digest, and a manifest digest is **assigned by a registry when something is pushed
-to it**. The control plane's image is built from source and pushed nowhere, so it has no such
-digest, so it cannot be named — and a registry cannot be installed without a control plane to
-install it. That is not a pin. It is a dependency the pinning rule created by accident.
-
-**It went unnoticed because the lab hid it.** The lab raised a disposable registry, stocked it from
-a workstation, and rewrote every image reference to point at it — so the lab bootstrapped along a
-path no real machine has. A first node in the lab always worked, and a first node anywhere else had
-no path at all. Every bootstrap fault found this year was found late for the same reason: **the
-install procedure existed only as a test fixture**, and a fixture is free to invent what it needs.
-
-## Considered Options
-
-**1. Publish the mesh's own images to a public registry.** Rejected on the premise: there is no
-public registry for the mesh's modules and there is not meant to be. It would also make raising a
-mesh depend on somebody continuing to host its code, which is the dependency the whole arrangement
-exists to remove.
-
-**2. Build the control plane from source on the first machine.** Rejected. A bare machine would
-need a toolchain and a working tree — and worse, the source lives in a forge **that runs on the
-mesh**. A total rebuild would then need the mesh it is rebuilding. Acceptable for adding a node to
-a healthy mesh; useless for the case that matters.
-
-**3. Keep a disposable registry as an install step.** Rejected. It exists in no production, and
-concealing this problem is precisely what it has been doing.
-
-**4. Pivot through a temporary control plane.** Chosen.
-
-## Decision
-
-**An image may be named by the digest of its own configuration.** A bare `sha256:…` names an image
-the machine already holds — content-addressed, immutable, unforgeable, and requiring nothing to
-have served it. It satisfies what the pinning rule asks for; the rule simply never contemplated an
-image that no registry had ever seen. It is legal exactly where nothing could have served one.
-
-**Genesis is a pivot**, in this order:
-
-1. the installer **carries the control-plane image** and loads it onto the machine
-2. a **temporary** control plane is raised from it, named by that image's own digest
-3. the **registry module is installed** — its image is upstream and it is never built, which
- [`04-ISSUES/029`](../04-ISSUES/029-the-artifact-store-cannot-be-delivered-by-the-artifact-store/00-report.md)
- already settled: a module that provides the artifact store cannot be delivered through it
-4. the control-plane image is **pushed into the mesh's own registry**, which assigns it a manifest
- digest — the first one it has ever had
-5. the control plane is **reinstalled as an ordinary module** pinned to that digest
-
-**The host performs the replacement, not the control plane.** Tier 0 outlives tier 2: the control
-plane composes a declaration naming the registry-pinned image, and the host applies it and recreates
-the container. Nothing is asked to replace itself while running, and the control plane is stateless
-— what it knows is in the store.
-
-**The installer is tier 0, and a separate program from the host.** Bootstrapping is by hand and
-changes the machine, which is tier 0's definition. But the host states that it *connects to nothing
-and listens on nothing*, and that claim is what makes the one thing running forever on every
-machine auditable. An installer connects to plenty. Same tier, same delivery, different program.
-
-## Consequences
-
-- **The control plane stops being a special case.** It becomes an ordinary module with an ordinary
- image in the mesh's own registry — so the mesh can build and roll out **its own upgrades**, which
- is what a mesh that runs itself was always reaching for.
-- **The bundle's job shrinks** to raising a temporary control plane exactly once.
-- **The source builds the installer; it does not run it.** Cloning moves to a release machine, where
- a forge being available is an ordinary working assumption, and leaves the disaster-recovery path
- where it very much is not.
-- **The lab's disposable registry is deleted.** The lab bootstraps by running the same program a
- bare machine runs — the only arrangement in which the installer cannot quietly drift out of truth
- again.
-- **Two public images remain at genesis** — the store and the broker. An air-gapped install would
- embed those too, at a much larger artifact; that is a build variant, not a different design.
-- **A control-plane module manifest must exist**, and did not.
-- **The registry may require nothing.**
- [`04-ISSUES/029`](../04-ISSUES/029-the-artifact-store-cannot-be-delivered-by-the-artifact-store/00-report.md)
- states that a module providing the artifact store may not *build* artifacts, because there is
- nowhere to put them until it runs. The pivot shows that is the narrow case of a wider rule: **it
- may not require anything the store is needed to deliver.** Found the hard way — an unrelated
- change gave the registry a public name and, with it, a route requirement. At genesis nothing
- provides a route, and nothing can, because the routing stack needs images and images need the
- store. The same cycle, re-entered through a door the existing wording did not cover.
-- **The handover is the sharp edge.** For one moment the bundle and the module both describe the
- same container, and the host tracks what it owns. If a safe handover is not expressible with what
- exists, the install **stops before it** and says what is missing. A machine left without a control
- plane cannot be fixed remotely, so a partial install that halts cleanly is the better outcome.
-
-**How each is checked** — an unenforced rule is indistinguishable from a wrong one:
-
-- **Naming by its own digest:** a machine that can reach no registry at all raises a control plane.
-- **The pivot completed:** after installing, the running control plane's image is pinned by a digest
- **the mesh's own registry assigned** — not by an image id. If it is still the image id, the pivot
- did not happen and the mesh cannot upgrade itself.
-- **No fiction left in the lab:** the scenario declares no registry machine, and the bed bootstraps
- through the installer rather than around it.
-- **The handover:** a machine whose control plane has been replaced still has one, and it answers.
-- **The registry requires nothing:** its manifest is resolvable on a mesh that has no other module
- in it. A requirement added to it later is caught where it is written, rather than by a genesis
- that cannot complete — which is how this one was found.
-
-## References
-
-- [ADR 0006 — the substrate and the control plane](0006-the-substrate-and-the-control-plane.md),
- which pinned images by digest and named what a first node fetches.
-- [ADR 0005 — the node host](0005-the-node-host.md), which makes tier 0 the one thing installed by
- hand and the only thing that changes a machine — the property this keeps true by shipping the
- installer beside the host rather than inside it.
-- [`04-ISSUES/029`](../04-ISSUES/029-the-artifact-store-cannot-be-delivered-by-the-artifact-store/00-report.md),
- the same cycle one layer down, and the rule that a registry module is named and never built.
diff --git a/02-DECISIONS/0068-the-lab-takes-requests.md b/02-DECISIONS/0068-the-lab-takes-requests.md
deleted file mode 100644
index f5ef5e43..00000000
--- a/02-DECISIONS/0068-the-lab-takes-requests.md
+++ /dev/null
@@ -1,119 +0,0 @@
----
-topic: building it
-status: superseded
-date: 2026-09-12
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0016-the-lab.md
-superseded-by: 02-DECISIONS/0149-the-live-mesh-is-the-test-bed.md
----
-
-# 68. The lab takes requests, one at a time, and runs each from its own copy
-
-> **Superseded — 2026-09-30, by [ADR 0149](0149-the-live-mesh-is-the-test-bed.md).** Never built. The
-> live mesh became the test bed, because the faults that cost the most are faults of a mesh that
-> already exists — bound consumers, containers made against an older roster, an adopted machine — and a
-> bed is by construction a mesh that does not. The rule worth keeping from below is that a run reads a
-> copy that is not anybody's working tree.
-
-## Context
-
-**The lab is exclusive hardware, and today a person holds it.** Raising a scenario takes over
-addresses and names on the workstation for as long as it stands, and only one scenario can stand
-at a time. So a run is not merely slow — it occupies the machine and the person who started it,
-who then waits rather than works.
-
-**Running it in the background against the working copy is worse than waiting.** The obvious fix
-is to start a run and carry on editing. But a run reads the working copy as it goes: binaries are
-rebuilt from it, manifests are read out of it, and the bed's own code is loaded from it. Edit
-while it runs and the result describes a state that never existed — a mixture of what was there
-when each file happened to be read. A green result obtained that way is not evidence, and a red
-one costs a day to disbelieve.
-
-**Nothing today records what was asked for.** A run is a command line in somebody's terminal. What
-commit it exercised, what it was trying to find out, and what it answered all live in scrollback,
-which is why the same question gets re-run rather than looked up.
-
-**Most of the parts already exist.** The lab writes a receipt of its last run. The mesh already
-carries messages between nodes and can notify a person. The machine already runs work on a
-schedule. What is missing is the thing in the middle.
-
-## Considered Options
-
-**1. Leave it as it is — a person drives the lab and waits.** Rejected. It is the loop
-[ADR 0010](0010-delivery.md) removed everywhere else, kept here by habit rather than by argument,
-and the cost compounds: because a run is expensive to start and blocks the person, fewer are run,
-so faults are found later and in larger batches.
-
-**2. Run in the background against the working copy.** Rejected on the reasoning above. The
-failure is silent, which is the kind this repository exists to refuse.
-
-**3. Put the lab behind the ordinary build pipeline.** Rejected for now. The pipeline builds
-artifacts and does not own a machine that can raise virtual machines; giving it one makes the
-pipeline's slowest job the lab's, and couples every push to hardware only one machine has. This
-may become right later; it is not the smallest thing that works.
-
-**4. A queue in front of the lab, and an isolated copy behind it.** Chosen.
-
-## Decision
-
-**The lab accepts requests rather than commands.** A request is recorded, queued, and answered.
-The person who made it is told when it is answered and does not wait.
-
-**A request names a bed and a commit, and nothing else.** This is the load-bearing restriction. A
-request may say *run this bed, at this version of these repositories*. It may not say what to
-install, on which machine, or with which settings — because a request that could say those things
-would be a second way of installing a mesh, and the whole reason the installer exists is that the
-lab already was one ([ADR 0067](0067-genesis-is-a-pivot.md)). The bed decides what is installed;
-the request only decides which bed and which version.
-
-**Requests are released one at a time.** The hardware admits one standing scenario, so the queue
-enforces what the hardware already requires, rather than leaving it to whoever remembers.
-
-**Every run happens in a copy the lab owns.** The lab checks the requested commit out into its own
-path and builds and runs from there. A working copy is never read by a run. This is what makes the
-queue safe to use while work continues, and without it the rest of this record is not worth
-having.
-
-**The lab is reached through tools, not only a command line.** A command line is available only
-to whoever is sitting at the machine, which is the constraint this record exists to remove. The
-lab answers three questions to anything that can reach the mesh — *what is standing now*, *what is
-queued or running*, and *what did this request answer* — and accepts a request and a cancellation.
-An agent can therefore start a run, stop attending to it, and come back; and somebody who did not
-start a run can still see it, which is the difference between a shared lab and a private one.
-
-**The restriction holds at every door.** A tool submits a bed and a commit, exactly as a command
-line does. A tool that could name a module, a node or a setting would reintroduce the second
-installer through a different entrance, and the entrance is not what made it dangerous.
-
-**Every run leaves a record that outlives the terminal**: what was asked, which commit, when it
-ran, what it answered, and where its output went. A question already answered is looked up rather
-than re-run.
-
-## Consequences
-
-Work continues while the lab runs, which is the point. A second session may edit freely, because
-nothing it edits is what the lab is reading.
-
-A request is reproducible by construction: it names a commit, so the same request can be asked
-again and compared. Today two runs of "the same thing" are only as alike as the tree happened to be.
-
-The lab gains a second copy of every repository it exercises, costing disk and needing to be kept
-from drifting into a place people edit by hand.
-
-Anything that can reach the mesh can now see what the lab is doing, including an agent working on
-something else. That is the intended gain and also the obvious hazard: a thing that is easy to ask
-is easy to ask too often, and the hardware still admits one scenario at a time.
-
-The queue becomes a thing that can fail — stuck, backed up, or lost — and a queue nobody watches
-is worse than no queue, because it absorbs requests silently.
-
-## How this is checked
-
-| Rule | Checked by |
-|---|---|
-| A run never reads a working copy | The runner is given a path it owns and no other; a run started while a working copy is deliberately dirtied produces a result matching the commit, not the edits. |
-| One scenario stands at a time | A second request submitted while one runs is observed to wait, not to raise. |
-| A request cannot say what to install | The request format admits a bed and a commit only. A request naming a module, a node or a setting is refused, and the refusal is exercised. |
-| A request is answered | Every queued request reaches a terminal state with a record. A request that vanishes is a failure of the queue, not a quiet nothing. |
-| The lab can be asked from elsewhere | What is standing is asked from a session that did not raise it, and the answer matches the machine. A lab that only answers its own caller has not left the terminal. |
diff --git a/02-DECISIONS/0069-a-module-is-a-repository-and-a-path.md b/02-DECISIONS/0069-a-module-is-a-repository-and-a-path.md
deleted file mode 100644
index 82fa411a..00000000
--- a/02-DECISIONS/0069-a-module-is-a-repository-and-a-path.md
+++ /dev/null
@@ -1,89 +0,0 @@
----
-topic: building it
-status: accepted
-date: 2026-09-12
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0009-modules-and-the-graph.md
----
-
-# 69. A module is a repository and a path within it
-
-## Context
-
-**The builder clones one repository and reads `module.json` at its root.** The to-be design says
-so in as many words — *"one file at the root"* — and the code implements it: clone, read the root
-manifest, build what it declares.
-
-**Nothing that exists is shaped that way.** The catalogue holds sixty-seven modules, each in its
-own directory, and has no manifest at its root. None of the five code repositories has one either.
-So today the builder cannot be asked to build any module that exists: pointed at the catalogue it
-finds no manifest, and pointed at a module's source it finds no manifest.
-
-**The system being replaced already works the other way**, and has for years: a monorepo with one
-directory per piece of software, and the coordinator builds a module from a repository and a path
-inside it. The root-only assumption is not a simplification of that — it is a different model that
-was never reconciled with it.
-
-**And it splits what a build needs into two places.** The control plane's manifest sits in the
-catalogue; the source it describes sits in the control plane's own repository. A build must read
-one tree, so under the root-only model neither location can be built from.
-
-## Considered Options
-
-**1. One repository per module.** Rejected. Sixty-seven repositories for sixty-seven modules, most
-of which are a single manifest naming a public image, and every one needing its own creation,
-permissions and lifecycle. It also contradicts [ADR 0015](0015-applications-live-in-their-own-repository.md),
-which put *applications* in their own repositories precisely because modules do not need one.
-
-**2. Keep manifests in the catalogue and source elsewhere, and have a build fetch both.** Rejected.
-A build would clone two trees whose versions can disagree, so "what commit is this module?" stops
-having one answer — and that question is the whole basis of knowing when to rebuild.
-
-**3. A module is a repository and a path within it.** Chosen. It is what the current system does,
-what the catalogue already looks like, and it keeps a module's description beside the thing it
-describes.
-
-## Decision
-
-**A module is named by a repository and a path within it.** The path holds `module.json`, and
-everything that manifest declares is produced from that path. A module whose path is the root is
-the ordinary case of this, not a separate one.
-
-**A module's manifest lives beside its source.** Where a module has code, its directory holds both,
-so one commit answers "what is this module, and what is it made of". Where a module has no source —
-a manifest naming a public image — the directory holds only the manifest, and there is nothing to
-build.
-
-**This moves the core modules.** The control plane and the builder are built from the control
-plane's repository, so their manifests belong in that repository at their own paths, not in the
-catalogue. The catalogue keeps the modules whose source it holds, and the modules that are only a
-manifest.
-
-**One commit, one module version.** Because a module is one path in one repository, the commit that
-built it identifies it exactly, and "the source has moved ahead of what the mesh holds" stays a
-question with a yes or no answer.
-
-## Consequences
-
-The builder gains a path alongside the repository and the ref. A build is `repository, path, ref`,
-and the manifest it returns is the module the mesh records.
-
-The catalogue stops being the place every manifest lives, and becomes the place manifests live
-*when their module has no other home*. That is a smaller claim than it sounds: most of the
-sixty-seven stay exactly where they are.
-
-Two repositories change shape — the control plane's gains manifests for the modules built from it.
-Nothing else moves.
-
-A repository can hold modules that are built and modules that are not, and no rule distinguishes
-them beyond whether their manifest declares anything to build.
-
-## How this is checked
-
-| Rule | Checked by |
-|---|---|
-| A module is buildable from its repository and path | The builder is asked for a module by repository and path, and returns a manifest whose artifacts are pinned to digests the mesh's registry assigned. |
-| A manifest sits beside what it describes | A module declaring something to build, whose path holds no source to build it from, is refused at build time rather than producing an empty result. |
-| One commit identifies one module | Two builds of the same repository, path and commit produce the same digests. |
-| The core modules are built like any other | The control plane is rebuilt from its own repository and path, and the running mesh is upgraded to it — the same path an ordinary module takes. |
diff --git a/02-DECISIONS/0070-the-catalogue-owns-the-module-graph.md b/02-DECISIONS/0070-the-catalogue-owns-the-module-graph.md
deleted file mode 100644
index ce5bba0e..00000000
--- a/02-DECISIONS/0070-the-catalogue-owns-the-module-graph.md
+++ /dev/null
@@ -1,107 +0,0 @@
----
-topic: the tiers
-status: accepted
-date: 2026-09-12
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0067-genesis-is-a-pivot.md
----
-
-# 70. The catalogue owns the module graph, and genesis builds rather than carries
-
-## Context
-
-**The module graph has no owner.** What modules exist, what each requires and provides, what each
-claims, what each is made of — all of it lives inside the control plane because that is where it
-was first written, not because anything decided it belonged there.
-
-**The control plane's own test says it does not belong there.**
-[`06-the-control-plane`](../03-DESIGN/01-to-be/06-the-controller.md) defines the tier as
-*everything that needs to know about more than one node*, and states the corollary plainly:
-anything a single machine could answer alone is not the control plane's. What a module is, and
-what it needs, requires no knowledge of any node whatsoever.
-
-**And nothing can query it.** The graph is the thing that answers *what must be rebuilt when this
-changes*, *what would break if this were removed*, and *what can be installed here* — and today it
-is reachable only as control-plane internals. [ADR 0009](0009-modules-and-the-graph.md) already
-decided that a build edge is derived, that an artifact is stale when anything it was built against
-moved, and that the rebuild set is therefore computable. None of that has anywhere to live.
-
-**Genesis currently carries an image, and cannot produce a builder at all.**
-[ADR 0067](0067-genesis-is-a-pivot.md) has the installer carry the control plane's image. That
-works, but it leaves the builder with no route onto a fresh mesh — it cannot be fetched from the
-public internet, because the mesh builds it, and the installer carries one image only. So a raised
-mesh cannot build anything, including the modules it is made of.
-
-## Decision
-
-**The catalogue is a core module, beside the control plane and the builder, and it owns the module
-graph.** Modules, what they require and provide, what they claim, their dependencies and their
-build edges, assignments and configuration — the catalogue holds them and serves tools over them:
-install a module, query what is available and what it needs, query the graph, query and update
-settings.
-
-**It is one per mesh**, expressed the way the control plane already expresses it — a claim scoped
-to the mesh, not a new mechanism.
-
-**The control plane consumes it.** Resolving what a module requires into an actual binding, and
-composing what a machine should be, both need to know what modules are. So the dependency runs
-from the control plane to the catalogue, which is the opposite of what the tiers suggest and is
-therefore written down here rather than left to be inferred.
-
-**Genesis builds the core modules rather than carrying them.** The installer ships an *init
-builder* — the one thing carried — which is started, clones the source, and builds the control
-plane, the catalogue and the builder. The installer then raises a temporary control plane, which
-installs the catalogue, registers the permanent control plane and the builder, and assigns all
-three to the first machine. The temporary control plane stops; the installer verifies that the
-permanent one answers and can query the graph. The builder then sees a catalogue in its initial
-state and builds the core modules into it.
-
-**So exactly one thing is carried, and it is a builder rather than a result.** That is the
-difference from [ADR 0067](0067-genesis-is-a-pivot.md), which carried the control plane's image:
-carrying a builder produces every core module on the machine, including the builder itself, so
-there is no component left without a route.
-
-## Consequences
-
-The catalogue joins the small set of things that cannot arrive through the ordinary path, because
-it cannot be installed by something that needs it in order to install anything. It arrives the same
-way everything else does under this record — built by the init builder before the mesh can install
-anything — so the set is answered by one mechanism rather than three special cases.
-
-The rebuild fan-out gains a home. *What was this built against* and *what must rebuild now* are
-questions about the graph, and the graph now has an owner to hold the edges and answer them.
-
-The catalogue holds state, so it owns a store in the substrate's database, the same way the control
-plane's contexts do. That is the mesh's own store and not the `postgres` module, which is a
-provider other modules consume.
-
-A mesh without a catalogue cannot resolve anything, where previously it merely lacked an interface.
-That is the cost of ownership over surfacing, and it is deliberate: one owner beats two copies.
-
-## Open, and to be settled before this is built
-
-**Where the init builder clones from.** [ADR 0067](0067-genesis-is-a-pivot.md) rejected building
-from source at genesis partly because the source lives in a forge that runs on the mesh, so a
-total rebuild would need the mesh it is rebuilding. Carrying a builder answers the toolchain half
-of that objection and not this half. Genesis must therefore name a source that exists before the
-mesh does.
-
-**What the init builder publishes into.** Building produces artifacts that must be pinned by a
-digest a registry assigned, and today the registry is installed after the machine has joined. If
-the core modules are built first, the registry has to exist first, so the substrate's order needs
-restating rather than assumed.
-
-**Where the line falls between the catalogue and the control plane.** Claims and assignments need
-to know about every node, which by the control plane's own test is its work. Whether the catalogue
-holds them and asks, or the control plane holds them and the catalogue surfaces them, is not
-settled here.
-
-## How this is checked
-
-| Rule | Checked by |
-|---|---|
-| The catalogue owns the graph | The control plane answers *what does this module require* by asking the catalogue, and a mesh whose catalogue is stopped cannot resolve — observed, not assumed. |
-| One per mesh | A second catalogue assigned anywhere in the mesh is refused by the claim, and the refusal is exercised. |
-| Genesis builds rather than carries | The installer carries exactly one artifact, and after installing, every core module is pinned to a digest the mesh's own registry assigned. |
-| The builder has a route | A mesh raised by the installer, with no hand-placed image, can build a module. |
diff --git a/02-DECISIONS/0071-where-genesis-gets-its-source.md b/02-DECISIONS/0071-where-genesis-gets-its-source.md
deleted file mode 100644
index 3fdcc03f..00000000
--- a/02-DECISIONS/0071-where-genesis-gets-its-source.md
+++ /dev/null
@@ -1,80 +0,0 @@
----
-topic: the tiers
-status: accepted
-date: 2026-09-12
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0070-the-catalogue-owns-the-module-graph.md
----
-
-# 71. Genesis clones from a mesh, and checks what it got
-
-## Context
-
-**[ADR 0070](0070-the-catalogue-owns-the-module-graph.md) has the init builder clone the source,
-and does not say from where.** [ADR 0067](0067-genesis-is-a-pivot.md) had already rejected building
-at genesis partly for that reason: the forge holding the source runs *on* the mesh, so a total
-rebuild would need the mesh it is rebuilding.
-
-That objection is real but narrower than it reads. It only binds when the mesh being raised and the
-mesh holding the source are the same one, which is true exactly once.
-
-## Decision
-
-**Genesis clones from a mesh's forge, reached by name.** Any mesh that holds the source can serve
-it. The first mesh is not structurally special — it is simply the only one that existed when there
-was nothing else to clone from.
-
-**If the mesh serving the source is lost, the name moves to another mesh that holds a copy.**
-Recovery is a name pointing somewhere else, not a backup being restored. This is what makes the
-source's survival a property of there being more than one mesh, rather than a property of somebody
-having remembered to take a copy. A mesh that has installed from that name holds the source
-afterwards, so every installation adds a place the name could point.
-
-**Genesis names a commit and checks what it got.** It does not clone whatever a branch happens to
-point at. The forge a mesh installs from is the trust anchor for everything that mesh will ever
-run, and a branch is a moving target that somebody else controls.
-
-*This is not hypothetical.* On 2026-09-11 the forge that would serve this role was running a
-cryptominer, and its git operations were being tampered with in flight — output injected into the
-protocol stream by a hook that fired on every fetch. Nothing was altered: the repositories were
-verified against local copies and found byte-identical. But a mesh installing from that name during
-those hours had no way to establish that for itself, and would have had none.
-
-## Consequences
-
-The init builder needs a name it can resolve and a commit it can verify, and nothing else. It does
-not need to know which mesh answers.
-
-Whoever operates the mesh that name points at carries a responsibility to everyone installing from
-it, and should know that. It is not merely a convenience host.
-
-A mesh that cannot reach any forge cannot be raised. That is a real limit and it is accepted: the
-alternative is carrying the whole source in the installer, which makes the installer a release
-artifact that goes stale rather than a program that fetches what it was told to.
-
-## Open — what relationship a mesh keeps afterwards
-
-**Not decided, and named here so it is not decided by accident** by whoever writes the init
-builder. Two shapes, and they are meaningfully different:
-
-**A snapshot, and then independence.** A mesh installs once, mirrors the source into its own forge,
-and has no upstream afterwards. It is fully self-hosted, in the sense that nothing it needs lives
-anywhere else. Updates are then something an operator does deliberately, by pulling changes in —
-tooling for which is possible and is not a priority.
-
-**A continuing upstream for core modules**, the way a distribution serves packages and a separate
-collection serves everything else. A mesh keeps looking at the origin for the modules that make a
-mesh a mesh, and holds its own for the rest.
-
-The first is more obviously aligned with the rest of this design, which is arranged so nothing a
-mesh needs depends on somebody else continuing to host it. The second is more convenient and makes
-a security problem in one forge everybody's problem. Neither is chosen here.
-
-## How this is checked
-
-| Rule | Checked by |
-|---|---|
-| Genesis needs only a name and a commit | A mesh is raised with the name pointed at a different mesh than the last time, and the result is identical. |
-| What was cloned is what was asked for | Genesis is pointed at a commit and refuses a forge serving different content under it, rather than building what it received. |
-| Losing the serving mesh is survivable | The name is repointed at a mesh that installed from it earlier, and a raise succeeds. |
diff --git a/02-DECISIONS/0072-two-graphs-and-the-build-chain.md b/02-DECISIONS/0072-two-graphs-and-the-build-chain.md
deleted file mode 100644
index 45a69152..00000000
--- a/02-DECISIONS/0072-two-graphs-and-the-build-chain.md
+++ /dev/null
@@ -1,86 +0,0 @@
----
-topic: the tiers
-status: accepted
-date: 2026-09-12
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0070-the-catalogue-owns-the-module-graph.md
----
-
-# 72. Two graphs, and a build chain that orders itself
-
-## Context
-
-[ADR 0070](0070-the-catalogue-owns-the-module-graph.md) gave the catalogue the module graph and
-said the control plane consumes it — that resolving a requirement and composing what a machine
-should be *"both need to know what modules are, so the dependency runs from the control plane to
-the catalogue."*
-
-**That paragraph is wrong, and this record corrects it.** It was written before the two graphs had
-been told apart, and it creates a dependency that does not need to exist: a catalogue that is down
-would leave the control plane unable to compose the declaration that would repair it.
-
-## Decision
-
-**There are two graphs, with different owners, and they meet only when something is installed.**
-
-| Graph | Owner | What it links | Answers |
-|---|---|---|---|
-| the module graph | the **catalogue** | module-versions to each other | what was this built against · what must rebuild now · what does this need |
-| the runtime graph | the **control plane** | module-versions to nodes | what runs where · who consumes this provision · what breaks if this machine goes |
-
-The catalogue does not know nodes exist. The control plane holds module-versions and nodes, along
-with capabilities and claims, because deciding whether a machine qualifies needs every node.
-
-**So the control plane never asks the catalogue anything.** It holds what it needs to compose a
-declaration. A catalogue that is down stops new installs and stops the rebuild fan-out, and does not
-touch anything already running or the ability to repair it.
-
-**Everything between them travels as events over the broker**, like all module-to-module
-communication. The builder finishes and announces that a module was built. The catalogue registers
-it, places it among what it depends on, and announces that a module was upgraded. The control plane
-reacts to *that*, not to build output — a semantic fact rather than an artifact.
-
-**Nothing is lost if a receiver is down.** A consuming module's queue is durable with a dead-letter
-exchange, declared by the mesh rather than by the module, so an event waits for a consumer that is
-not there.
-
-**Build order is not computed. It emerges from the chain.** The builder never consults the graph: it
-builds what it is asked for, one at a time. The catalogue asks for the next build after the previous
-registration, so *"do not start this until that is registered"* holds by construction rather than by
-a schedule somebody maintains.
-
-**The catalogue's rule is a condition, not an order:** ask for a module to be rebuilt once everything
-it was built against is current. That handles a chain and a diamond with one rule, where an
-order-based approach needs to know the shape in advance.
-
-**Two refusals belong to the catalogue.** A cycle, because the chain would never settle. And a
-rebuild whose artifacts are identical to what it replaced, which is not an upgrade and must not be
-announced as one — or a single change ripples outward forever through modules that did not change.
-
-**Genesis does none of this.** The init builder has a fixed, short list — control plane, catalogue,
-builder — in a written order, because there is no catalogue yet to ask.
-
-## Consequences
-
-The builder stays simple, and independent of the catalogue. That is what makes genesis possible at
-all: the thing that builds the catalogue cannot require the catalogue.
-
-The two sides can be briefly out of step — the catalogue may hold a module-version a moment before
-the control plane knows of it. Assigning in that instant fails, and should say why rather than
-report that no such module exists.
-
-A module's declared events stop being documentation and become its permissions: an account is
-scoped from what a module emits and consumes, so a builder that announces what it built is granted
-what it needs by the ordinary mechanism rather than by a special case.
-
-## Open
-
-**Whether an upgrade is applied or merely noticed.** Today the system this replaces deploys
-automatically, and that is a defensible default for core modules on a mesh its operator runs. But
-the design as it stands does the opposite: it records that the source moved ahead, makes it visible,
-and waits to be told. This must become a setting with a chosen default rather than inherited
-behaviour — and the choice matters most on the day a bad commit reaches something that carries mail.
-
-**Whether a module assigned to several machines upgrades on all of them at once.** Doing so makes
-one bad commit simultaneous everywhere. Doing one machine and pausing turns it into one casualty.
diff --git a/02-DECISIONS/0073-the-installer-carries-a-builder.md b/02-DECISIONS/0073-the-installer-carries-a-builder.md
deleted file mode 100644
index c83ac552..00000000
--- a/02-DECISIONS/0073-the-installer-carries-a-builder.md
+++ /dev/null
@@ -1,94 +0,0 @@
----
-topic: the tiers
-status: accepted
-date: 2026-09-13
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0070-the-catalogue-owns-the-module-graph.md
----
-
-# 73. The installer carries a builder, and the registry stays where it is
-
-## Context
-
-[ADR 0070](0070-the-catalogue-owns-the-module-graph.md) decided that genesis builds rather than
-carries, and [ADR 0071](0071-where-genesis-gets-its-source.md) settled where it clones from. Two
-questions were left open, and the design record names them as the one gap that stops a fresh mesh
-from being able to produce anything at all: **how the builder arrives**, and **what it publishes
-into**.
-
-Today the installer carries the control plane's image inside itself. That works, and it is why
-genesis needs no registry: nothing is ever fetched, because the one image that matters is already
-present. The cost is that the mesh which results holds an artifact it did not make, cannot rebuild,
-and knows nothing about — no version, no source, no edges. That is the same shape as the fault
-[issue 044](../04-ISSUES/044-the-runtime-every-module-builds-on-cannot-be-built-by-the-mesh/00-report.md)
-recorded for the shared runtime, and fixing it there while shipping it here on every new mesh would
-be a strange place to stop.
-
-## Decision
-
-**The installer carries a builder, and nothing else.** One artifact, not a growing set. It clones
-the source at a named commit, checks what it got ([ADR 0071](0071-where-genesis-gets-its-source.md)),
-and produces the control plane from the same repository and path that any later rebuild of it would
-use. What raises the mesh is therefore the same thing that will maintain it, and there is no second
-mechanism kept in step with the first.
-
-**The registry does not move, and the argument for moving it does not survive being made.**
-
-It was put this way: a produced image has to be put somewhere before anything can fetch it, so the
-registry must now precede the control plane, and
-[ADR 0033](0033-the-substrate-is-a-store-and-a-broker.md)'s answer to that question has to flip.
-
-It does not, because the premise is false. **The thing that builds the image and the machine that
-runs it are the same machine.** A built image is already in that machine's container runtime, and
-the temporary control plane names it exactly as it names a carried one — by the digest of its own
-configuration, a local identity that requires nothing to have served it. Building changes where the
-bytes came from. It does not change where they are.
-
-| | is it substrate? | must it precede the control plane? |
-|---|---|---|
-| the store | yes | yes — there is nowhere else to put the control plane's state |
-| the broker | yes | yes — the control plane reaches a machine only over it |
-| the image registry | yes — it cannot grant itself a repository | **still no** — the first machine neither fetches the control plane nor needs to, whether the image was carried in or made here |
-
-So the registry stays where [ADR 0033](0033-the-substrate-is-a-store-and-a-broker.md) put it:
-substrate by role, ordinary by delivery, installed by the temporary control plane as its first act.
-The bundle carries two services and a control plane, as it did. **What publishes into the registry
-is unchanged too** — the existing step that pushes the control plane's image into it, which is the
-moment that image first receives a digest assigned by something other than itself. It now pushes
-something this mesh built rather than something it was handed.
-
-## Consequences
-
-**Genesis gains one step and changes no others.** A build happens before the image is loaded. The
-pivot described in [ADR 0067](0067-genesis-is-a-pivot.md) survives exactly as written, because the
-step it pivots on never cared where the image came from.
-
-**A fresh mesh can produce from the moment it exists.** The builder is present before the control
-plane is, so the core modules, the catalogue and the builder's own module can be built in the
-ordinary way rather than waiting for somebody to carry them in. The paragraphs in
-[`17-raising-a-mesh`](../03-DESIGN/01-to-be/17-raising-a-mesh.md) that describe this were describing
-something that could not start; they can start now.
-
-**Genesis needs more of the outside world.** Carrying an image needed nothing but the installer.
-Building one needs the source, and whatever the build itself reaches for. This is a real cost and
-is not waved away: it makes genesis fail in more ways, all of them at a step that says what it was
-doing. It is accepted because the alternative is a mesh that cannot rebuild its own control plane,
-which fails in exactly one way, silently, later, and for ever.
-
-**A pre-built bundle remains possible and is not this.** Nothing here forbids delivering artifacts
-rather than building them; it fixes where they may come from. A bundle of pre-built core modules is
-an **export of a mesh that built them**, carrying what the catalogue knows about each alongside the
-artifact itself — so that loading one leaves the graph in the state building would have left it. A
-bundle that carries images without that is the thing this decision rejects, whoever ships it.
-
-## What this does not decide
-
-**Whether the builder's own module is carried or built.** It builds everything else; what installs
-*it* as an ordinary module afterwards, so that it too can be upgraded, is the same closed-list
-question [`12-a-module-repository`](../03-DESIGN/01-to-be/12-a-module-repository.md) already holds,
-and is unchanged by this.
-
-**How a machine authenticates to a registry that asks it to.** Genesis raises its own and reaches it
-over the loopback, so this remains a joining problem
-([issue 042](../04-ISSUES/042-nothing-gives-a-node-an-account-for-a-registry/00-report.md)).
diff --git a/02-DECISIONS/0074-the-wire-is-specified-not-the-types.md b/02-DECISIONS/0074-the-wire-is-specified-not-the-types.md
deleted file mode 100644
index f0287a1f..00000000
--- a/02-DECISIONS/0074-the-wire-is-specified-not-the-types.md
+++ /dev/null
@@ -1,172 +0,0 @@
----
-topic: the tiers
-status: accepted
-date: 2026-09-15
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0039-what-the-sdk-holds-and-refuses.md
----
-
-# 74. The mesh defines a module protocol; an SDK is an implementation of it
-
-## Context
-
-[ADR 0039](0039-what-the-sdk-holds-and-refuses.md) says what the SDK holds: the tool-serving
-harness, the messaging and event framework, the contracts, and core primitives. It settles what
-belongs in *an* SDK. It does not say what happens when there is more than one.
-
-There is already more than one. **The contracts are expressed twice** — as Go types in the control
-plane and the host, and as TypeScript types in the SDK — and nobody has felt it because both live
-in one repository and one head.
-
-**A correction, made after inspecting the wire rather than the types** (2026-09-16). This record
-first claimed the two implementations already disagreed — `resource` vs `Provision`, `consumer`
-meaning the module in one and the node in the other, headers declared on one side and emitted by
-neither. **On inspection the live wire agrees**, and the claim was wrong:
-
-- The grant types that disagreed (`Grant`, `Interface`, `Credential` in the SDK's `contracts`)
- were **dead** — exported and imported by nothing. The live provisioning wire is the contributions
- file, whose shape (`as`, `secret`, `node`, `at`, `values`) is the same on both sides. Those dead
- types have been removed.
-- The envelope agrees too: Go emits all five required headers, and `x-causation-id`/`x-schema` are
- **optional** — the SDK sets them when a handler has a causation or a schema, and a bare event
- carrying neither is correct, not a drift.
-
-So the danger was never live disagreement. It was **dead types that contradicted the live wire**,
-which read as the contract and were not — and are exactly what led this record to assert a drift
-that inspection did not find. That is a sharper reason for the decision below, not a weaker one: a
-type is only as good as its being the wire, and the way to guarantee that is to specify the wire and
-check implementations against it, rather than to trust a hand-kept type to still describe it.
-
-A failure of this kind does not announce itself. Two implementations that disagree about an
-envelope do not fail to compile — they ignore each other's messages, and a mesh where a module
-stops reacting looks exactly like a mesh where nothing happened.
-
-## The question this settles
-
-A module may be written in any language the mesh can build
-([`18-building-a-module`](../03-DESIGN/01-to-be/18-building-a-module.md)). Every language needs an
-SDK. What is an SDK *of*?
-
-Two answers were available, and the obvious one is wrong.
-
-**Shared types, generated.** Write the shapes once — a schema, an IDL — and generate Go, TypeScript,
-Rust. It is the familiar answer and it solves the smaller half of the problem. The shapes are not
-where the difficulty is.
-
-**A specified wire, with a conformance suite.** The shapes are a consequence; what an SDK must get
-right is *behaviour*.
-
-## Decision
-
-**The mesh defines a module protocol. An SDK is an implementation of that protocol in one
-language, and nothing more.**
-
-That is the whole of what an SDK is. Not a library a language happens to have, not a convenience
-layer, not a place for helpers to accumulate — an implementation of a specified protocol, finished
-when it implements it and correct when it agrees with every other implementation.
-
-### The protocol is split per capability
-
-**A module does not use all of it, so an SDK need not implement all of it.** A module that only
-consumes events uses the event capability. One that serves tools uses the tool capability. A
-provider uses provisioning. Nothing about consuming an event requires knowing how a grant is
-answered.
-
-So the protocol is a floor plus capabilities:
-
-| part | what it covers | who needs it |
-|---|---|---|
-| **connection** — the floor | reading the sealed credential, pinning the certificate fingerprint, taking identity from the credential rather than the environment | everything |
-| **events** | the envelope and its headers, the durable per-consumer queue, binding, at-least-once with dedup on `x-event-id` | a module that emits or consumes |
-| **tools** | registration, the shared durable `serve.` queue, request and reply | a module with a surface |
-| **provisioning** | a grant in, a credential out, and what each carries | a module that provides something |
-
-**This is the same shape the host already has.** A host declares which resource kinds it can apply,
-and a partial host — one that can write files and run things but not manage users or containers —
-is a real thing rather than a broken one ([ADR 0005](0005-the-node-host.md)). An SDK that implements
-the floor and events is exactly as legitimate, and a module written against it is a module that
-does events.
-
-**So a language arrives in pieces rather than all at once.** A Rust SDK implementing connection and
-events is useful the day it exists; tools and provisioning follow when something needs them. The
-alternative — a language is unsupported until it is entirely supported — is what makes adding one a
-project rather than a contribution.
-
-**And what a language can be used for is then a fact the mesh can state**, rather than something an
-author discovers by writing a module that cannot be built: the toolchain list says which languages
-exist, and the conformance results say what each can do.
-
-### What the specification covers
-
-Per capability, what two implementations can disagree about:
-
-- **the exchanges and queues** — which exchanges exist, that a consumer's queue is durable and
- named `..events`, that a tool is served from a shared durable `serve.`
-- **the envelope** — every header, which are required, what an unknown `x-` header means, and that
- ignoring one is correct rather than lax
-- **identity** — that a module's node and module name come from its sealed credential and not from
- its environment, so what it emits matches what the mesh authorised
-- **delivery** — at-least-once, and that dedup is on `x-event-id`, which only the emitter can make
-- **the credential** — the sealed document's fields, and that a connection pins a certificate
- fingerprint rather than trusting an authority
-- **provisioning** — a grant in, a credential out, and what each carries
-- **the vocabulary** — that `consumer` is one thing, named once
-
-### Conformance is per capability
-
-**A suite per part, and an SDK claims the parts it passes.** A monolithic pass/fail would make a
-partial implementation indistinguishable from a broken one, which is the distinction this is built
-on.
-
-**And the suite is executable, not prose.** A specification nobody can run is a document two
-implementations drift from while both believe they conform. Conformance is a set of fixtures — an
-emitted event, a served tool call, a grant and its answer — that every SDK must produce and consume
-byte-for-byte.
-
-**The existing two implementations are the first two to be made to pass it.** Not a future language:
-the drift above is present, and a suite that only new SDKs must satisfy would leave the disagreement
-that already exists in place while certifying everything added afterwards against it.
-
-## Why not generated types
-
-Generation makes the shapes agree and leaves everything that matters unspecified. Two SDKs
-generated from one schema can still name their queues differently, take identity from the
-environment, dedup on the wrong field, or omit a header the other requires — and every one of those
-is a mesh that runs and quietly does not work.
-
-It also makes the contract into whatever the generator supports, which is a decision nobody made
-about a boundary everything else depends on.
-
-**The shapes are worth generating once the wire is specified.** That is a convenience, and it comes
-second.
-
-## Consequences
-
-**A language is a commitment, and now a divisible one.** Adding one means implementing the protocol
-and passing the suites for the parts it claims. That is more work than transliterating types, and
-it is the work that was always there — the difference is that it can be finished, and finished in
-pieces, rather than believed.
-
-**Versioning becomes possible.** `x-schema` exists for it and is never written. A specified envelope
-with a version on the body is what lets a mesh hold a module built against an older SDK, which is
-the ordinary state of any mesh that has been running for a while.
-
-**The two current implementations agree on the live wire** — inspection showed it. What was wrong
-was a set of dead types beside the wire, now removed. The suite's job here is therefore prevention:
-to keep that agreement true as the wire changes, and to hold a new language's SDK to it, rather than
-to repair a break that exists today.
-
-**This does not make the mesh polyglot by itself**, and should not be reported as though it does. It
-makes polyglot possible to do correctly. A Rust SDK is still a Rust SDK.
-
-## How this is checked
-
-| Rule | Checked by |
-|---|---|
-| One vocabulary | A word means one thing across implementations, checked by the fixtures using it. |
-| The wire is what is specified | Both existing SDKs run the conformance suite in their own test suites, and a change to one that breaks a fixture fails there rather than in a mesh. |
-| A new SDK is a passing SDK | A language is not listed as buildable for a capability until its SDK passes that capability's suite; the toolchain list and the conformance results name the same set. |
-| A partial SDK is a real thing | An SDK implementing the floor and one capability passes, is listed for that capability, and a module using another is refused with the reason — rather than failing at runtime in a language nobody said was finished. |
-| An unknown header is ignored | A fixture carries one, and every implementation accepts it. |
-| Identity comes from the credential | A fixture sets an environment that disagrees with the credential, and the emitted event carries the credential's. |
diff --git a/02-DECISIONS/0075-two-stores-and-which-provides-what.md b/02-DECISIONS/0075-two-stores-and-which-provides-what.md
deleted file mode 100644
index 5205d385..00000000
--- a/02-DECISIONS/0075-two-stores-and-which-provides-what.md
+++ /dev/null
@@ -1,129 +0,0 @@
----
-topic: the tiers
-status: accepted
-date: 2026-09-15
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0014-no-npm-workspace.md
----
-
-# 75. An artifact store is a provision; a package registry is a different one
-
-## Context
-
-Two questions have been circling, and they turn out to be one question asked twice.
-
-**"Should gitea be the mesh's registry?"** It serves OCI images and a dozen package ecosystems, it
-is already needed — genesis clones from one — and the mesh's own registry has neither
-authentication ([issue 042](../04-ISSUES/042-nothing-gives-a-node-an-account-for-a-registry/00-report.md))
-nor a transport a runtime will accept over a network
-([issue 048](../04-ISSUES/048-nothing-makes-a-machine-trust-the-mesh-registry/00-report.md)). Gitea
-has both.
-
-**"Where does the SDK come from?"** [ADR 0014](0014-no-npm-workspace.md) already answers it — each
-module consumes its dependencies, the mesh's own shared library included, *from the private
-registry* — and nothing installs one, so today it comes from a git URL, which is
-[issue 053](../04-ISSUES/053-the-sdk-is-pinned-twice-and-the-two-disagree/00-report.md).
-
-**The framing that dissolves both:** `artifact-store` is already a provision, and `registry`
-already provides it. So "should gitea be the registry" is not a question about replacing a
-component. It is a question about **a second provider of an existing provision** — which this mesh
-has a mechanism for, and uses for certificate authorities and VPNs already.
-
-## Decision
-
-**Two provisions, because they are two jobs.**
-
-| provision | is | for |
-|---|---|---|
-| `artifact-store` | content-addressed blobs, pinned by digest, no versions, no ranges | what the **mesh** delivers to **machines** |
-| `package-registry` | an ecosystem's own registry — npm, cargo, PyPI, Go | what **code** resolves when it is compiled |
-
-They are not the same store with different clients. One is addressed by digest and immutable by
-construction; the other is addressed by name and version, and resolves ranges. Conflating them is
-how a mesh that pins everything ends up rebuilding one commit into two different things.
-
-**`registry` remains the provider genesis installs.** Not because it is better, but because of what
-it is: a directory and one container, no database, no control plane, installable at step 8 of an
-install where neither exists yet. Gitea needs a store and provisioning, which means a control plane,
-which means the pivot has already happened — and the pivot needs somewhere to publish to.
-
-**Gitea also provides `artifact-store`, and a mesh may choose it.** Two providers of one provision
-is a thing the mesh understands: it refuses, names both, and choosing is assigning the one you want.
-
-**And it does not claim `the-artifact-store`.** That claim is node-scoped, so a module holding it
-cannot share a machine with another that does — and a machine running gitea for git and packages
-*alongside* a registry serving artifacts is an ordinary arrangement, not a conflict. They are
-different ports doing different jobs.
-
-The exclusivity that matters is mesh-wide and is already expressed: `provides` at mesh scope means
-two providers are two answers, and the resolver refuses until one is assigned. Forbidding
-co-residence adds nothing to that and forbids something reasonable. **Whether `registry` should
-still hold that claim is left open here** — it may be protecting something about the port or the
-data directory that is not written down, and removing a claim is not a thing to do from the outside
-of a manifest.
-A mesh that assigns gitea gets authentication and TLS for its artifacts — which is to say, **issues
-042 and 048 are answered by choosing a provider that already solved them**, rather than by
-reimplementing accounts and certificates in a registry that has none.
-
-**Gitea provides `package-registry`.** That is ADR 0014's private registry, and it is one service
-rather than one per ecosystem. `verdaccio` may provide it too, for npm alone, and is then a choice
-somebody makes rather than the answer.
-
-**The registry is not removed at the end of installing.** A mesh that never runs gitea still has an
-artifact store. Retiring it is a migration a mesh performs, not a step an installation ends with.
-
-## Why not simply gitea, from the start
-
-Because genesis would need a control plane before the thing that stores the control plane's image,
-and that is circular rather than merely awkward. It would also make one of the three things the
-build loop cannot produce for itself into a stateful application with a database — the pivot is the
-hardest part of this design already.
-
-And it puts every artifact in the service that is also the trust anchor for everything the mesh will
-ever run ([ADR 0071](0071-where-genesis-gets-its-source.md)), which records that forge serving a
-cryptominer with tampered git operations. Two blast radii are better than one.
-
-## Moving from one provider to the other is a designed act
-
-**Not a removal.** Every image a machine runs is pinned to a digest at a named store, the control
-plane's own included. Changing the provider means:
-
-1. gitea installed, reachable, and holding an account the builder may publish with
-2. every artifact mirrored
-3. every declaration re-pinned, the control plane's **last**, because it is what performs the others
-4. **every machine verified to have converged and to be able to pull from the new store**
-5. only then the old provider unassigned, and its volume kept ([ADR 0030](0030-data-outlives-the-mesh-that-declared-it.md))
-
-Step 4 is the one that is easy to skip and the only thing between this and a mesh that cannot
-restart its own control plane. A machine that reboots mid-migration pulls from a store that no
-longer exists, and a local image cache hides that until exactly the moment it matters.
-
-## Consequences
-
-**The bootstrap is unchanged**, which is the point of keeping the small provider.
-
-**042 and 048 gain a second answer.** They can be fixed in the registry, or dissolved by choosing a
-provider that already has accounts and TLS. The second is less work and more service.
-
-**ADR 0014 becomes satisfiable.** There is a provision for the private registry, something that
-provides it, and a module may depend on it — so the SDK can be published and consumed rather than
-cloned, and issue 053 has somewhere to go.
-
-**A mesh can be minimal or complete, and both are legitimate.** One with the small registry and no
-gitea builds and runs modules and cannot serve packages. That is a real configuration, not a broken
-one — the same way a partial host is real.
-
-**And the bootstrap still has no package registry.** The first build of the shared base happens
-before anything has installed one. That is the same pivot as everything else and it is **not solved
-here**: it is named, so the next person does not discover it.
-
-## How this is checked
-
-| Rule | Checked by |
-|---|---|
-| Genesis needs no database | The installer raises a mesh of one on a machine with nothing, and the artifact store it installs has no store of its own. |
-| Two providers are a choice, not a conflict | A mesh holding both is asked to resolve `artifact-store` and refuses, naming both, until one is assigned. |
-| Providers may share a machine | A node is assigned both gitea and a registry, and both run — only one of them answers `artifact-store`. |
-| The two stores are not interchangeable | A module depending on `package-registry` is not satisfied by `artifact-store`, and the refusal says why. |
-| A migration is verified before it is finished | The old provider cannot be unassigned while any machine's declaration still names it. |
diff --git a/02-DECISIONS/0076-the-sdk-is-a-published-package.md b/02-DECISIONS/0076-the-sdk-is-a-published-package.md
deleted file mode 100644
index ffd1ae81..00000000
--- a/02-DECISIONS/0076-the-sdk-is-a-published-package.md
+++ /dev/null
@@ -1,87 +0,0 @@
----
-topic: building it
-status: accepted
-date: 2026-09-16
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0075-two-stores-and-which-provides-what.md
----
-
-# 76. The SDK is a published package, and the toolchain resolves it by version
-
-## Context
-
-[ADR 0014](0014-no-npm-workspace.md) decided a module consumes its dependencies — the mesh's own
-shared library included — from the private registry. [ADR 0075](0075-two-stores-and-which-provides-what.md)
-decided the private registry is a `package-registry` provision, and that gitea provides it. What
-neither settled, and what [issue 053](../04-ISSUES/053-the-sdk-is-pinned-twice-and-the-two-disagree/00-report.md)
-left open, is the one build where the rule cannot simply be obeyed: **the first one.**
-
-The TypeScript toolchain image is built *from* the SDK — it carries the SDK so that every module
-compiled inside it resolves the shared library without each build fetching it. So the thing that
-compiles TypeScript and the thing that contains the SDK were the same object, and that object
-cannot be what builds the SDK. Stated as a question — "how does the SDK reach the registry before
-the toolchain exists, when the toolchain is what builds it?" — it reads as a paradox.
-
-It is not one. The paradox exists only because the toolchain *bakes a git-cloned copy* of the SDK.
-The SDK itself is plain TypeScript: it needs `node` and `tsc` and nothing the mesh makes. A public
-base image can build it. The circularity is a property of the workaround, not of the SDK.
-
-## Decision
-
-**The SDK is an ordinary published package in the mesh's `package-registry`, consumed by version.**
-The git URL in the toolchain's manifest and the sibling-path lock beside it — the two halves of
-issue 053 — are both removed. A build resolves the SDK the way it resolves any dependency, with a
-lock that agrees with its manifest, so `npm ci` is the command and reproducibility is by
-construction rather than by the machine the build ran on.
-
-**The SDK is built with a public base image, not with the mesh's toolchain.** It is *not* one of
-the components the loop cannot build — the control plane, the registry, the builder, the catalogue
-([`12-a-module-repository`](../03-DESIGN/01-to-be/12-a-module-repository.md)), which arrive by
-carrying an init builder because they are the loop's own machinery. The SDK is machinery for
-nothing; it is an ordinary dependency the loop builds and publishes like any other. The only
-constraint is narrow: it cannot be compiled *in the mesh toolchain*, because that toolchain is built
-from it. So it is compiled on a public base image instead — which needs nothing the mesh makes — and
-published before the toolchain that consumes it. It is not carried, because building it does not
-wait on a mesh existing first.
-
-**The toolchain base stays, thinned.** mesh-tools remains the image bundles are compiled in and the
-one place the SDK is resolved — but it `npm ci`s the SDK by version from the registry instead of
-baking a copy cloned from a git URL. Bundles keep borrowing its resolved dependencies; what changes
-is that the version they borrow is named and honest. This was the shape chosen over dropping the
-shared base entirely and having every bundle resolve the SDK itself: one resolution point, one
-place to be right about the version.
-
-**Genesis orders the publish before the first compile.** The package-registry provider is a public
-image (gitea), so it comes up needing no toolchain; the SDK is published into it; only then is the
-toolchain built, so the first `npm ci` has a registry to read from. Nothing in that chain is
-circular, because the only thing that needed the toolchain — baking the SDK — is gone.
-
-## Consequences
-
-Each language's toolchain repeats the shape: its own SDK, built from that language's public base
-image, published to the same registry, resolved by version with that ecosystem's lockfile-honest
-install (`npm ci`, `cargo` against a vendored or registry source, `pip` against a pinned set). The
-warning in issue 053 — that whatever the TypeScript repository does the others will copy — is
-answered by making the copied thing the correct one.
-
-A change to the SDK is publish-then-consume, exactly as [ADR 0014](0014-no-npm-workspace.md) already
-priced it: publish the new SDK version, then bump the toolchain (and any module pinning it directly)
-to consume it. There is no shortcut that resolves an unpublished SDK, which is the property that was
-missing.
-
-mesh-tools is no longer an SDK carrier in the sense that mattered — it does not contain a copy
-whose provenance is a branch head somebody force-pushes. It contains a version.
-
-A mesh with no package-registry cannot build TypeScript. This is accepted and is not new: it is the
-same shape as a mesh that cannot reach a forge being unable to be raised
-([ADR 0071](0071-where-genesis-gets-its-source.md)). Installing brings the registry up first.
-
-## How this is checked
-
-| Rule | Checked by |
-|---|---|
-| The SDK a build compiles against is named, not cloned from a branch | The toolchain manifest pins `@novox/mesh-sdk` to a version, and the build runs `npm ci`, which refuses a lock that disagrees with the manifest. Issue 053's two checks become this one. |
-| The SDK builds without the mesh's own toolchain | The SDK's build recipe names a public base image. A recipe that named the mesh toolchain would reintroduce the cycle and is refused in review. |
-| The registry is up before the first compile | The genesis bed asserts the package-registry answers, and the SDK is published, before the base build runs. A base build that ran first would fail its `npm ci` with no registry, which is the positive control. |
-| A second language repeats the shape, not a new one | When a second SDK is added, its recipe is compared to this one: public base, publish by version, lockfile-honest install. |
diff --git a/02-DECISIONS/0077-the-controller-and-the-foundation.md b/02-DECISIONS/0077-the-controller-and-the-foundation.md
deleted file mode 100644
index d9cc7ebd..00000000
--- a/02-DECISIONS/0077-the-controller-and-the-foundation.md
+++ /dev/null
@@ -1,61 +0,0 @@
----
-topic: the mesh
-status: accepted
-date: 2026-09-16
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0006-the-substrate-and-the-control-plane.md
----
-
-# 77. The parts are named controller, foundation, node — not control plane, substrate, master
-
-## Context
-
-The words drifted. In conversation and in code the same thing was called *control plane*,
-*controller*, *master*, and *hub*; the store-and-broker pair was called *substrate* and
-*foundation*; a machine was a *node*, a *worker-node*, a *peer*, a *slave*. A mesh named
-differently by two people is a mesh they describe differently, and the drift was worst on the
-parts talked about most.
-
-Three of the terms carried wrong ideas. *Control plane* is borrowed from networking's
-control-plane/data-plane split and means nothing here. *Master/slave* and *hub/peer* imply a
-subordinate — but no node is: a node applies its own declaration and keeps running when the
-control-node dies, so it is as much its own machine as any other. *Substrate* is a biology
-metaphor that landed for no one.
-
-## Considered Options
-
-1. **Keep the inherited words.** Rejected: they are the source of the drift, and two of them
- (control plane, substrate) are metaphors that teach the wrong shape to anyone reading them cold.
-2. **master / slave, or hub / peer, for the nodes.** Rejected: both name a hierarchy the mesh does
- not have. The control-node owns no other node; lose it and the rest keep running what they were
- last told.
-3. **controller / foundation / node + control-node.** Chosen.
-
-## Decision
-
-The component that decides what each node should be, holds the mesh's records, and tells nodes is
-the **controller** — the module `mesh-controller`, which claims the mesh-scoped `the-controller`
-seat. The store and broker raised at genesis are the **foundation**. Machines are **nodes**;
-there are 0..n of them, and exactly one — the one running the controller — is the **control-node**.
-
-Retired: *control plane*, *substrate*, *master/slave*, *hub/peer*, *worker-node*.
-[`00-META/glossary.md`](../00-META/glossary.md) is the authority, and a new name for an existing
-thing lands there in the change that introduces it in code.
-
-## Consequences
-
-`mesh-control` became `mesh-controller` across the module, container, image, binary, `cmd/` dir and
-the git repository; `substrate` became `foundation` in the embedded base bundles, the default
-template and the example lock; the seat `the-control-plane` became `the-controller`. The 03-DESIGN
-prose and 00-META follow the new words.
-
-What got harder: the records under `02-DECISIONS/` are immutable, so they keep the words they were
-written with — this record included, whose own title names what it retires. A term retired here
-still appears there, and the glossary is how to read it. The git repository on the forge was
-renamed `mesh-control` → `mesh-controller`.
-
-## References
-
-- [`00-META/glossary.md`](../00-META/glossary.md) — one name per thing, and the words retired.
-- The rename shipped across all six code repositories and hq (main).
diff --git a/02-DECISIONS/0078-the-store-and-broker-are-modules.md b/02-DECISIONS/0078-the-store-and-broker-are-modules.md
deleted file mode 100644
index 00c114b0..00000000
--- a/02-DECISIONS/0078-the-store-and-broker-are-modules.md
+++ /dev/null
@@ -1,63 +0,0 @@
----
-topic: the tiers
-status: accepted
-date: 2026-09-16
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0033-the-substrate-is-a-store-and-a-broker.md
----
-
-# 78. The store and the broker are ordinary modules
-
-## Context
-
-[ADR 0033](0033-the-substrate-is-a-store-and-a-broker.md) settled that the foundation is a store
-and a broker, raised at genesis; [ADR 0006](0006-the-substrate-and-the-control-plane.md) settled
-that the controller cannot grant itself either, because it consumes them and is not running yet to
-ask. Both were raised as bundle resources — plumbing, with no record in the mesh's module graph.
-
-That left two costs, named in [issue 051](../04-ISSUES/051-the-mesh-cannot-update-what-it-depends-on/00-report.md).
-The foundation's own store and broker could not be upgraded — nothing owned them as modules. And a
-mesh that wanted a database or a queue for its modules installed the `postgres`/`lavinmq` modules,
-each of which raised a **second** server: a mesh ran two postgres and two brokers.
-
-## Considered Options
-
-1. **Leave them as bundle-only plumbing.** Rejected: they cannot be upgraded, and the second
- server stays. The floor keeps a permanent specialty in it.
-2. **The control-plane pivot verbatim — raise a temporary one, install the module, retire the
- temporary** ([ADR 0067](0067-genesis-is-a-pivot.md)). Rejected for a *stateful* server: it means
- a handover with real downtime, tearing down the store the controller is mid-read of.
-3. **Adopt in place.** Chosen.
-
-## Decision
-
-The foundation's store and broker are **adopted in place** as the ordinary `postgres` and `lavinmq`
-modules. Genesis still raises them first (nothing else can — ADR 0006), then each module declares a
-container with the **same name, image and spec** the foundation raised, so the applier — which keys
-on the container name and compares a spec digest — reconciles it rather than raising a second. The
-credentials are the foundation's, made at genesis and carried in through `secret accept`, because
-the mesh cannot invent a credential that already made the databases. The servers bind mesh-wide so
-a consumer on any node can reach the one shared server.
-
-A mesh runs **one postgres and one lavinmq**, and each is upgradeable through a stated window: the
-store's is a connection-pool reconnect; the broker's is the harder case of recreating the bus the
-push travels over, so the mesh reconnects to the one that returns.
-
-## Consequences
-
-The twelve-module floor has no specialty left in it — the store and broker are moments in a
-module's life, not a separate kind of thing. Two follow-ups are tracked:
-[issue 054](../04-ISSUES/054-the-adopted-store-and-broker-are-open-before-the-filter/00-report.md)
-(the adopted servers bind `0.0.0.0` before the packet filter is installed) and
-[issue 055](../04-ISSUES/055-the-adopted-store-and-broker-may-be-reachable-on-one-node-only/00-report.md)
-(whether a consumer on another node reaches them over the overlay).
-
-What got harder: a foundation upgrade recreates the very server the controller reads from, or the
-bus the instruction to upgrade travels over — a window that a stateless module upgrade does not have.
-
-## References
-
-- [issue 051](../04-ISSUES/051-the-mesh-cannot-update-what-it-depends-on/00-report.md) — the gap this closes.
-- [`03-DESIGN/01-to-be/07-the-foundation.md`](../03-DESIGN/01-to-be/07-the-foundation.md) — the amended design.
-- Shipped across mesh-host, mesh-catalog and mesh-lab (main); proven 22/22 in the one-node lab.
diff --git a/02-DECISIONS/0079-the-foundation-seats-are-named-after-their-servers.md b/02-DECISIONS/0079-the-foundation-seats-are-named-after-their-servers.md
deleted file mode 100644
index 6ccae0e1..00000000
--- a/02-DECISIONS/0079-the-foundation-seats-are-named-after-their-servers.md
+++ /dev/null
@@ -1,63 +0,0 @@
----
-topic: the tiers
-status: accepted
-date: 2026-09-17
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0078-the-store-and-broker-are-modules.md
----
-
-# 79. The foundation seats are named after their servers
-
-## Context
-
-[ADR 0078](0078-the-store-and-broker-are-modules.md) settled that the store and broker are the
-ordinary `postgres` and `lavinmq` modules, adopted in place on the control-node, and that a mesh
-runs **one postgres and one lavinmq**. But that singularity held only by convention: genesis
-assigns them to the control-node alone. [Issue 056](../04-ISSUES/056-an-adopted-module-assigned-to-a-second-node-raises-a-second-server/00-report.md)
-recorded the gap — a second `assign` to another node finds no container of that name there and
-raises a *second* server, holding none of the first's data, and nothing refuses it.
-
-The mesh already has the mechanism for "there is one of me": a mesh-scoped exclusive **seat**.
-[ADR 0077](0077-the-controller-and-the-foundation.md) gave the controller one, which it named
-`the-controller`. Nothing gave the store and broker theirs.
-
-## Decision
-
-Each foundation module claims a mesh-scoped seat named **after the server it guards**:
-`mesh-controller`, `mesh-store`, `mesh-broker`. So the `postgres` module claims `mesh-store` and
-the `lavinmq` module claims `mesh-broker`; the resolver refuses a second holder mesh-wide, the same
-way it keeps one hub and one controller. A second `assign` is now a refusal at resolution — *one
-per mesh* — not a silent second server.
-
-The controller's seat, which [ADR 0077](0077-the-controller-and-the-foundation.md) named
-`the-controller`, is renamed `mesh-controller` under this same rule, so all three foundation seats
-follow one convention: the seat is the server. (The module `mesh-controller` and its seat now share
-a name, which is the point — there is one of that server, and the seat says so.)
-
-This does not tie a foundation module to the control-node — a mesh-scoped seat forbids a *second*
-holder, not a wrong single one. Adoption still requires the container to already be running where
-the module lands, which genesis arranges; the seat closes the "two servers" gap, and the
-control-node convention remains what puts the one holder in the right place.
-
-## How this is checked
-
-- The resolver's `checkClaims` refuses two holders of a mesh-scoped seat
- (`mesh-controller/internal/catalogue/resolve.go`).
-- `TestAFoundationModuleCannotBeRaisedOnASecondNode` asserts each foundation module's second
- assignment is refused with *one per mesh*.
-- The `postgres`, `lavinmq` and `mesh-controller` manifests declare the seat.
-
-## Consequences
-
-"One store, one broker, one controller" is now a property the mesh enforces rather than a
-convention it hopes for. The silent operational edge that remains — a cross-node consumer
-provisioned only when the provider is pushed again — is separate, and tracked as
-[issue 057](../04-ISSUES/057-a-cross-node-consumer-is-provisioned-only-when-the-provider-is-pushed-again/00-report.md).
-
-## References
-
-- [issue 056](../04-ISSUES/056-an-adopted-module-assigned-to-a-second-node-raises-a-second-server/00-report.md) — the gap this closes.
-- [ADR 0077](0077-the-controller-and-the-foundation.md) — named the controller's seat, here renamed.
-- [ADR 0078](0078-the-store-and-broker-are-modules.md) — asserted one postgres and one lavinmq, here enforced.
-- [`00-META/glossary.md`](../00-META/glossary.md) — seat and claim.
diff --git a/02-DECISIONS/0080-the-development-cycle-is-checked.md b/02-DECISIONS/0080-the-development-cycle-is-checked.md
deleted file mode 100644
index 50460553..00000000
--- a/02-DECISIONS/0080-the-development-cycle-is-checked.md
+++ /dev/null
@@ -1,79 +0,0 @@
----
-topic: how we work
-status: accepted
-date: 2026-09-17
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0019-how-this-repository-works.md
----
-
-# 80. The development cycle is checked, not trusted
-
-## Context
-
-[ADR 0019](0019-how-this-repository-works.md) made this repository the source of truth, and the
-process overview drew the flow work must follow: an idea or a symptom, a decision, a to-be design,
-a build in a code repository, an as-is update on shipping. The playbooks describe every step, and
-frontmatter carries every status.
-
-But the flow itself was enforced by nothing. A design could appear citing no decision; a design
-could sit `in-progress` naming no code; an issue could be `fixed` by nobody knows what. Each is
-indistinguishable from correct work until somebody reads carefully — and the whole point of the
-playbooks is that nobody should have to hold this repository in their head. A session that starts
-cold (or an agent after a context clear) must be able to *find* the chain by following frontmatter
-pointers, which only works if the pointers are reliably there.
-
-## Decision
-
-The development cycle is enforced mechanically, to the extent frontmatter can carry it:
-
-- **No design without a decision** — every to-be design names at least one record in `decisions:`.
-- **No development without a design that says where** — an `in-progress` or `implemented` design
- names its owning code in `code:`.
-- **No owner-less diagnosis, no fix-less fix** — an issue marked `located` or `fixed` names
- `located-in:`; one marked `fixed` or `resolved` says `fixed-by:` (prose counts — "nothing, the
- capability existed" is an answer).
-- **No silent graduation** — a `graduated` research overview says what it `became:`, and the
- targets exist.
-- **No two records answering to one number** — added 2026-09-30; see the insight below.
-
-> **Progressive insight — 2026-09-30.** The list above named four things `cycle.py` enforces, and
-> now names five. Nothing enforced that two issue records hold different numbers: two machines
-> filing issues within one hour both read `main`, both took "the next free number", and collided
-> twice — the second collision reaching `main` with `records.py`, `cycle.py` and `index.py` all
-> reporting success (04-ISSUES/155). A number is how every other record cites one, so two records
-> answering to it is a citation that resolves to whichever folder the reader opened. `cycle.py`
-> refuses it now. The decision here stands exactly as written: this is one more thing frontmatter
-> and file names can carry, found by its absence rather than by reasoning.
-
-> **Progressive insight — 2026-10-10.** The third rule above, no owner-less diagnosis and no fix-less
-> fix, and the replay rule `cycle.py` took on under ADR 0237, moved out of `cycle.py` the day slice 3 of
-> [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)
-> landed: an issue's status lives in the issue tracker, whose `move` refuses at the write what the
-> check refused at merge — `located` without an owner, `resolved` without a fix, a core issue without
-> its replay, `wontfix` without a reason. Enforced mechanically still, one step earlier, in the one
-> place the status is. `cycle.py` keeps the fifth rule, no two records answering to one number, and
-> refuses a status key in a report opened after the import. The decision stands: the cycle is checked,
-> not trusted; which program checks each rule is a fact that moved.
-
-> **Progressive insight — 2026-10-10, later.** `cycle.py` refuses a status key in any report, not only one
-> opened after the import: the keys the older reports carried were removed once the import was checked
-> against every folder (ADR 0275's last note).
-
-[`00-META/checks/cycle.py`](../00-META/checks/cycle.py) refuses violations, beside `records.py`
-and `index.py`; all three run before any merge here. What frontmatter cannot see — that code work
-actually started from a handoff — remains held by playbooks 04 and 07: a feature branch exists
-because a design or an issue sent it, and a merge is a human checkpoint.
-
-## Consequences
-
-A `/clear` costs little: [`AGENTS.md`](../AGENTS.md) now carries the cycle and a where-to-look
-table, and the chain a fresh session needs is guaranteed present in frontmatter rather than
-reconstructed from memory. The checks are the floor, not the ceiling — they verify pointers exist,
-not that their content is true; reading remains the job.
-
-## References
-
-- [`00-META/process/00-overview.md`](../00-META/process/00-overview.md) — the flow, and its new
- "The cycle is checked" section.
-- [ADR 0019](0019-how-this-repository-works.md) — the repository this disciplines.
diff --git a/02-DECISIONS/0081-a-decision-nothing-cites-is-not-yet-in-the-chain.md b/02-DECISIONS/0081-a-decision-nothing-cites-is-not-yet-in-the-chain.md
deleted file mode 100644
index 1f519992..00000000
--- a/02-DECISIONS/0081-a-decision-nothing-cites-is-not-yet-in-the-chain.md
+++ /dev/null
@@ -1,51 +0,0 @@
----
-topic: how we work
-status: accepted
-date: 2026-09-17
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0080-the-development-cycle-is-checked.md
----
-
-# 81. A decision nothing cites is not yet in the chain
-
-## Context
-
-[ADR 0080](0080-the-development-cycle-is-checked.md) made the development cycle checked — but its
-checks covered designs, issues and research, not the decisions themselves. Measuring showed why
-that matters: 19 of 70 records were cited by nothing — no design doc's `decisions:`, no research
-`became:`, no issue, no other record. Among them sat load-bearing decisions (the credential flow,
-the module-runtime cluster), and the cost had already been paid once in practice: a stale premise
-about an orphaned decision survived in working memory precisely because no pointer led to the
-record that had settled it.
-
-## Decision
-
-Every **accepted** decision must be reachable from the cycle: cited by a design doc's frontmatter
-(`decisions:` — a governing citation, not a prose mention), a research overview, an issue report,
-a `00-META` document, or another record's `extends`/`supersedes` chain.
-[`cycle.py`](../00-META/checks/cycle.py) refuses orphans. Proposed records are exempt — a record
-under consideration has no home yet — and superseded records are reachable through their
-supersession chain by construction.
-
-The 19 orphans were given true homes in the same change: the module-runtime cluster
-(0044–0049, 0053–0055) into the connectivity, controller, protocol, writing and model-access
-designs; the build decisions (0072, 0076) into the building design; 0021 into the playbook that
-implements it; the process records were already reachable once `00-META` counted as a source.
-
-Taken together with 0080, the practice has a name the industry will recognise:
-**spec-driven development, with provenance** — a decision is the *why*, the design doc is the
-spec, `code:` names the implementation, and the lab beds are the conformance tests. What the
-common form leaves implicit, the cycle makes checked: the spec itself must trace to a decision,
-and the decision must be findable from the work it governs.
-
-## Consequences
-
-Following pointers now reaches every accepted decision, so a cleared session (or a person) can
-trust the frontmatter graph as the whole map. The check is reachability, not truth: a citation
-placed wrongly still lies, and reading remains the job.
-
-## References
-
-- [ADR 0080](0080-the-development-cycle-is-checked.md) — the cycle this completes.
-- [`00-META/process/00-overview.md`](../00-META/process/00-overview.md) — the flow.
diff --git a/02-DECISIONS/0082-the-registry-is-reached-by-name-and-trusted-by-the-overlay.md b/02-DECISIONS/0082-the-registry-is-reached-by-name-and-trusted-by-the-overlay.md
deleted file mode 100644
index ed6964a8..00000000
--- a/02-DECISIONS/0082-the-registry-is-reached-by-name-and-trusted-by-the-overlay.md
+++ /dev/null
@@ -1,92 +0,0 @@
----
-topic: building it
-status: accepted
-date: 2026-09-17
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0072-two-graphs-and-the-build-chain.md
----
-
-# 82. The registry is reached by name, and the overlay is its security
-
-## Context
-
-Delivery ends at a node pulling an image, and for every node but the one that built it, that step
-has never worked. Three facts conspired, each recorded separately:
-
-- Artifact references are written under `127.0.0.1:5000` — a deliberate parking (the catalogue
- says so in the commit that reverted the mesh-reachable name: *"the runtime refused it: 'http:
- server gave HTTP response to HTTPS client' … the binding expression returns then"*), so a joined
- node is told to pull from its own loopback
- ([issue 048](../04-ISSUES/048-nothing-makes-a-machine-trust-the-mesh-registry/00-report.md)).
-- A container runtime treats any non-loopback registry as HTTPS, and the mesh's registry serves
- plain HTTP; nothing the mesh writes tells any runtime otherwise — the one place that file
- existed was the lab's, which is why this never failed in a bed (issue 048).
-- Nothing gives a node an account for the registry
- ([issue 042](../04-ISSUES/042-nothing-gives-a-node-an-account-for-a-registry/00-report.md)) —
- and nothing has ever said whether one is required.
-
-The tempting fix is TLS from the mesh's own authority. The machinery even exists — the mesh CA
-issues leaves for `.internal` names, and a `certificate:` manifest field delivers them. But
-the registry is reached **over the overlay**, and the overlay is WireGuard: every byte is already
-encrypted and already authenticated to a peer the mesh admitted. The mesh's own code has carried
-this position for weeks: *"the registry is reached over the mesh's own private network … a second
-layer inside it would be certificates to issue and rotate for no property the first does not
-have."* TLS inside the tunnel would also re-order genesis (a certificate needs the network, the
-registry precedes it) and put CA handling into three clients (the runtime, the archive fetcher,
-the builder) — cost with no new property.
-
-## Decision
-
-**The overlay is the registry's transport security, and the mesh writes the trust it means.**
-
-1. **References name the registry by its mesh name.** The builder publishes to
- `.internal:` (the binding's `at` and `port`) — the parked one-line change
- lands. Genesis still publishes to loopback on the first machine, before any overlay exists;
- references minted at genesis stay loopback and are valid where they matter — on that machine.
- A rebuild re-pins to the mesh name.
-2. **Every node on the private network is told the mesh's registry speaks plain HTTP.** The
- controller injects, into every such node's declaration, a merged `/etc/docker/daemon.json`
- naming `.internal:` under `insecure-registries`, and a service resource that
- restarts the runtime when that file changes — the `/etc/hosts` pattern for the content, the
- nftables pattern for the reload. No module author is involved; being on the network is what
- grants the trust, because being on the network is what the trust *is*.
-
- > **The mechanism changed — 2026-10-05, by [ADR 0222](0222-a-module-is-told-where-a-mesh-seats-holder-is-reached-and-the-controller-writes-no-file-a-seats-holder-owns.md).**
- > What stands: being on the network grants the trust, the overlay is the transport security, no
- > module author chooses it, and the trust is written into the runtime's file rather than over it
- > and reloaded rather than restarted (ADR 0102). What moved: the controller no longer injects the
- > file or the service. The container runtime's own module writes `insecure-registries`, told where
- > this machine reaches the store by `${seat:mesh-artifact-store:reach}`, because the controller
- > writes no file a seat's holder owns ([issue 190](../04-ISSUES/190-the-runtimes-configuration-is-written-by-modules-that-are-not-the-runtime/00-report.md)).
-3. **No accounts (issue 042), recorded as the position it always was.** Reading and pushing
- require presence on the overlay and nothing else. The boundary is enforced, not assumed: the
- registry's `listens` is `from: mesh`, the firewall derives from it, and the overlay admits only
- peers holding mesh-issued keys. An operator's *external* registry credential is an ordinary
- operator-supplied secret (`secret accept`), owned by whichever module names that registry.
- Per-node accounts return as a decision, not a patch, if the boundary assumption ever changes —
- the shape would be the existing provision flow.
-
-## How this is checked
-
-- The no-fake lab bed: a joined node pulls a mesh-built image by the registry's `.internal` name
- over the overlay — an address outside every range the lab's own runtime configuration trusts,
- so the mesh-written trust is what makes it work or nothing does.
-- The firewall half is generated from `listens` and visible in the node's ruleset (`from: mesh`).
-- The plaintext-inside-tunnel position holds exactly as long as the registry is unreachable off
- the overlay; the `listens` stanza and the derived ruleset are the check.
-
-## Consequences
-
-A runtime restart when the trust first lands on a node — at joining, before workloads, where it
-is free. The registry's HTTP is exposed to whatever stands on the overlay, which is the stated
-boundary; a mesh that wants defence in depth inside its own tunnel reopens this record rather
-than bolting certificates on quietly. Issues 042 and 048 close on this record.
-
-## References
-
-- [issue 042](../04-ISSUES/042-nothing-gives-a-node-an-account-for-a-registry/00-report.md),
- [issue 048](../04-ISSUES/048-nothing-makes-a-machine-trust-the-mesh-registry/00-report.md).
-- [ADR 0072](0072-two-graphs-and-the-build-chain.md) — the build chain this completes.
-- [ADR 0029](0029-a-network-is-a-shape-because-an-action-cannot-be-undone.md) — the overlay as a
- boundary.
diff --git a/02-DECISIONS/0083-one-push-leaves-the-mesh-consistent.md b/02-DECISIONS/0083-one-push-leaves-the-mesh-consistent.md
deleted file mode 100644
index b181df99..00000000
--- a/02-DECISIONS/0083-one-push-leaves-the-mesh-consistent.md
+++ /dev/null
@@ -1,66 +0,0 @@
----
-topic: the mesh
-status: accepted
-date: 2026-09-18
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0010-delivery.md
----
-
-# 83. One push leaves the mesh consistent
-
-## Context
-
-A provision is minted while composing the *consumer's* node; the provider's grant list is a pure
-read of secrets already issued from it. So assigning a cross-node consumer and pushing its node
-produced a consumer that retried forever against a provider that had never heard of it, until the
-provider's node was pushed a second time — an action with no signal to take, documented nowhere,
-and invisible whenever consumer and provider share a machine
-([issue 057](../04-ISSUES/057-a-cross-node-consumer-is-provisioned-only-when-the-provider-is-pushed-again/00-report.md)).
-
-Two remedies were on the table: **cascade** — a push also delivers to the machines its compose
-changed — or **report** — a push says "now push the provider" and leaves the act to the operator.
-
-## Decision
-
-A push finishes what it starts: after composing and sending the named node, the controller flushes
-every *other* machine that is now behind — whose declaration differs from what it was last sent —
-by name, in the push's own output, converging over a bounded number of rounds (a flushed send may
-itself mint).
-
-Behind is measured against what a machine was last *sent*, not against a before/after snapshot of
-this push. The mint that makes a provider behind happens when the consumer is assigned or its
-account issued — before `push` runs at all — so by push time the provider already differs from
-what it holds, with no in-command delta to detect. The only durable signal is "what it should be"
-versus "what it last received", which is the same comparison `push --behind` already makes.
-
-A machine behind for an unrelated reason is flushed by this too, and that is correct rather than a
-cost: a named push that knew a machine was behind and left it so would be the very silence this
-decision removes. The narrower reading — flush only what this push provably changed — was
-rejected because it cannot see a mint that a prior command performed, which is precisely the 057
-case.
-
-Reporting alone was rejected because it converts a derived fact the controller already holds into
-an operator obligation, and an obligation enforced by nothing is issue 057 restated. The
-declaration is computed from the whole mesh; delivering a mesh that is knowingly inconsistent and
-merely saying so would make "push succeeded" mean less than it says.
-
-> **The mechanism changed — 2026-10-05, by [ADR 0221](0221-a-push-sends-no-build-a-policy-or-a-plan-holds-back-except-to-the-machine-it-names.md).**
-> A named push still flushes every other machine that is behind, compared against what each was last
-> sent. A machine whose modules would move to a build their upgrade policy records, or that an open plan
-> has not sent it yet, is no longer flushed: the push names it and leaves it for `push `. "Behind
-> for an unrelated reason" no longer covers a held upgrade
-> ([issue 259](../04-ISSUES/259-a-named-push-sent-every-machine/00-report.md)).
-
-## Consequences
-
-- One push is sufficient for a cross-node consumer: the provider's grants arrive from the same
- act that minted the provision. The undocumented rule "push the provider node too" ceases to
- exist rather than becoming documentation.
-- A named push delivers to every machine that is behind, not only the one named — each named in
- the output, never silent. `push --behind` remains the way to reconcile the mesh without naming
- a node; a named push now carries the same guarantee for the machines its work touched and any
- others already waiting.
-- How this is checked: the built-store-cross-node bed registers a cross-node consumer, pushes
- only the consumer's node, and asserts the provider minted its vhost — the workaround push is
- removed, so a regression fails the bed.
diff --git a/02-DECISIONS/0084-which-provider-serves-a-consumer.md b/02-DECISIONS/0084-which-provider-serves-a-consumer.md
deleted file mode 100644
index d0ecf878..00000000
--- a/02-DECISIONS/0084-which-provider-serves-a-consumer.md
+++ /dev/null
@@ -1,119 +0,0 @@
----
-topic: what runs on it
-status: accepted
-date: 2026-09-20
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md
----
-
-# 84. Which provider serves a consumer, when the mesh runs more than one
-
-## Context
-
-[ADR 0027](0027-a-provision-names-what-the-consumer-is-coupled-to.md) settled what a provision
-is *named* for — the thing the consumer's code is coupled to, so `postgres-database` and
-`mssql-database` are different provisions and a wrong match is refused at resolution. It said one
-thing more, in passing, and left it: *"Two providers of `postgres-database` — a container on this
-node and a managed instance elsewhere — are interchangeable and should both match."* Naming was
-decided; **which of several providers serves a given consumer was not.**
-
-The mesh assumes there is only one to choose. Provisions are mesh-scoped: the adopted store is a
-single `mesh-store` a consumer on any node reaches, and `scope: "mesh"` is written into the
-provision definitions. **That assumption is false on day one, and was always meant to be.**
-Node-specific services delivered to the mesh is the plan, not an edge case:
-
-- Both control-capable nodes already run their **own** general-purpose relational store (the same
- engine, one instance each), serving that node's own applications.
-- Each runs its **own** SQL server, its **own** cache, its **own** object store. One node alone
- runs six separate relational-store instances, each raised by the module that needed it.
-- The one provision that is currently single — the identity provider, one instance on one node —
- already authenticates applications whose home is a **different** node.
-
-So several providers of one provision name genuinely coexist, and they are **not** interchangeable
-the way 0027's aside supposed. They differ by node, by the data they hold, and by locality. A
-consumer bound to the wrong one reads the wrong database, or takes a cross-node hop it did not
-need, or cannot be moved without silently rebinding. The model has no field in which to say which
-one. This is 0027's own fault — *a match that resolves and is wrong* — one level up: 0027 refused
-the wrong **dialect**; nothing refuses, or even asks about, the wrong **instance**.
-
-## Considered Options
-
-1. **Keep `scope: "mesh"` — one provider per provision, mesh-wide.** Rejected: it is false on day
- one, and making it true would force every node's applications onto one node's server — the
- exact opposite of node-specific services delivered to the mesh, and a single point of failure
- the topology was built to avoid.
-2. **Resolve to any provider of the name (0027's "both match").** Rejected: when providers hold
- different data and live on different nodes they are not interchangeable, and picking one
- arbitrarily is a wrong-instance match — the confidently-wrong answer 0027 exists to prevent,
- restated at the level of the instance rather than the dialect.
-3. **Always require the consumer to name the provider explicitly.** Rejected: needless ceremony in
- the common case, where the consumer wants the provider on its own node; and a field every
- manifest must carry is a field an author forgets, which then matches everything again — the
- failure 0027 warned about for qualifiers.
-4. **A provision is node-scoped; the consumer selects the provider, defaulting to co-location.**
- Adopted.
-
-## Decision
-
-**A provision is served by a provider identified by its node, and the consumer selects which one.**
-A provider is a (node, module) pair, not a mesh-wide singleton. A consumer's binding resolves to a
-specific provider, and the selection is part of the assignment
-([ADR 0046](0046-a-module-configuration-is-its-assignments-not-its-manifest.md)), not the
-manifest.
-
-**The default is co-location.** A consumer that names no provider is served by the provider of
-that provision on **its own node**. This is the common case and needs nothing said. A mesh with
-one provider of a kind is just the case where co-location and "the only one" coincide — expressed
-as *there happens to be one*, not as a scope.
-
-**A consumer coupled to a provider's data names it.** Where two consumers must share one database,
-or a consumer must reach a provider on another node, the assignment names that provider — because
-that coupling is exactly what may not be guessed, and naming it is what makes a later move safe.
-
-**A module need not consume a provider at all.** It may carry its **own** instance inside its own
-composition — on its own module network, publishing no host port, **not** declared as a provision —
-when a genuine engine fork or a pinned server version makes the shared provider unusable. Such an
-instance is invisible to resolution and can be bound by nothing else. The rule is *share by
-default; embed only when a fork or a version forces it* — most of the per-module stores that exist
-today are vanilla engines on stale pins that a consolidation onto the node's provider would absorb.
-
-## Consequences
-
-The mesh can carry its real topology **deliberately** rather than by the accident of which
-provider happened to be the single one. A consumer's data-coupling becomes a stated fact, which is
-what lets a provider be moved without a consumer silently following the wrong one — provided a
-provider keeps its identity across a relocation, which is a follow-up this record opens rather than
-closes. Rotation ([13](../03-DESIGN/01-to-be/13-credentials-and-their-rotation.md)) addresses a
-specific provider's holders, not "the provision's".
-
-What got harder: an assignment now may carry a provider selection, and a wrong one is a new way to
-misconfigure. It is mitigated the way 0027 mitigated its own: the co-location default removes the
-choice in the common case, and genuine ambiguity — several providers, none named, none co-located —
-is refused with the candidates named, never resolved by picking.
-
-## References
-
-- [ADR 0027](0027-a-provision-names-what-the-consumer-is-coupled-to.md) — the naming this extends;
- its "both match" aside is the gap closed here.
-- [ADR 0031](0031-the-control-plane-authenticates-nobody.md) — identity is exactly such a
- provider, and already serves consumers on another node.
-- [ADR 0046](0046-a-module-configuration-is-its-assignments-not-its-manifest.md) — where the
- selection lives.
-- [ADR 0078](0078-the-store-and-broker-are-modules.md) — the adopted store, whose mesh-wide
- binding is the single-provider assumption this record replaces.
-- [issue 067](../04-ISSUES/067-a-provision-cannot-name-which-provider-serves-it/00-report.md) — the
- gap, and the day-one evidence.
-- [`03-DESIGN/01-to-be/23-choosing-a-provider.md`](../03-DESIGN/01-to-be/23-choosing-a-provider.md)
- — the design.
-
-> **Widened, 2026-10-01 (issue #258).** The pin named a node, on the reasoning that "the same
-> module on two machines is two answers, and which machine is the whole question". Half right:
-> two modules on one machine can both answer a provision — `public-acme` and `step-ca` both offer
-> `acme-ca` on novox — and then which *module* is the whole question, and a node alone cannot ask
-> it. A provider is a (node, module) pair ([design 23](../03-DESIGN/01-to-be/23-choosing-a-provider.md)),
-> and a pin now names the pair: `pin `. The resolver
-> refuses a node that answers twice instead of taking the last one listed, and refuses two
-> providers beside the consumer instead of settling them by a map walk — the same stance design 23
-> takes: ambiguity is refused, never resolved by picking. Records made before are completed by
-> migration where the node they name answers once. mesh-controller: `feat/pin-names-the-provider`.
diff --git a/02-DECISIONS/0085-a-secret-is-a-provision.md b/02-DECISIONS/0085-a-secret-is-a-provision.md
deleted file mode 100644
index f3222317..00000000
--- a/02-DECISIONS/0085-a-secret-is-a-provision.md
+++ /dev/null
@@ -1,153 +0,0 @@
----
-topic: what runs on it
-status: accepted
-date: 2026-09-20
-amended: 2026-09-20
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0031-the-control-plane-authenticates-nobody.md
----
-
-# 85. A secret is a provision, and the vault is the module that provides it
-
-## Context
-
-[ADR 0031](0031-the-control-plane-authenticates-nobody.md) decided that identity *runs on the
-mesh, not of it* — a module other modules require, rather than a privileged part of the
-controller. [ADR 0078](0078-the-store-and-broker-are-modules.md) did the same for the store and
-the broker: the twelve-module floor has no specialty left in it. **Secrets are the exception that
-survived.** No module owns a secret.
-
-Secret handling is smeared across three built-in parts of the runtime:
-
-- the **controller mints** one credential per consumer↔provider pair and seals it to both node
- keys ([ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md));
-- the **mesh generates local secrets** — *"generated secrets are the mesh's, never authored"*
- (as-is, `06-configuration-and-secrets.md`) — for a value a single module needs for its own use;
-- the **synchroniser injects** both into a node's generated files.
-
-Nothing is the owner of "a secret" the way the store module is the owner of "a database", and the
-cost is recorded rather than hypothetical. The as-is design names the weakness in its own words:
-*"Rotation is not a mesh operation… there is no mechanism that rotates one and informs everything
-holding it."* A `rotate` command has since been built and proven, but it reaches **only** the
-provisioned pairs; a secret a module generates for its own fully-local use — the password of a
-version-pinned embedded store ([ADR 0084](0084-which-provider-serves-a-consumer.md)), an internal
-token — is minted by the mesh and then has no operation that can remake it. Three species of
-secret, and only the first has an owner:
-
-| species | minted by | rotates? |
-|---|---|---|
-| a provisioned credential (a database login) | controller, sealed to nodes (0048) | yes — `rotate`, per pair |
-| a module's own local secret | the mesh, as a generated value | **no owner, no rotation** |
-| an operator-delivered secret (an external key) | a person, sealed in (`secret accept`) | no rotation, no audit |
-
-## Considered Options
-
-1. **Leave it a property of the controller.** Rejected: it is the smear above — no owner, local
- secrets that cannot be rotated, operator secrets that cannot be audited — and it is exactly the
- specialty 0078 removed for the store and broker, kept here for no reason anyone recorded.
-2. **A dedicated vault built into the foundation, not a module.** Rejected: it reintroduces a
- privileged built-in, the thing 0031 and 0078 went out of their way to remove, and a mesh that
- wants none would still carry it.
-3. **Fold all minting, the controller's provisioning credentials included, into the vault.**
- Rejected: the controller must mint in order to **deliver** any provision — the vault's own
- credential among them — so making the vault mint the credential of its own delivery is the
- store/broker chicken-and-egg for no gain. The provisioned-pair credential already has an owner
- and a rotation ([13](../03-DESIGN/01-to-be/13-credentials-and-their-rotation.md)); this record
- does not disturb it.
-4. **A secret is a provision; the vault is an ordinary module that provides it.** Adopted.
-
-## Decision
-
-**Secret-holding is a module, parallel to identity.** A module that needs a secret **for its own
-use** — a local service's password, an internal token, an external key it was handed — requires a
-`secret` provision from a vault provider, exactly as it requires a database from the store. The
-vault generates the value (or holds one it was given), and because the credential belongs to the
-consumer↔vault pair it **rotates, backs up and is audited through the same per-pair machinery**
-[13](../03-DESIGN/01-to-be/13-credentials-and-their-rotation.md) already defines. That is what
-gives the second and third species the rotation and audit they lack. A module's own secret stops
-being a generated value that nothing owns and becomes an ordinary provision with a provider.
-
-**The controller's minting of provisioning-pair credentials is unchanged** ([ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md)):
-it is how every provision, the vault's own included, is delivered. The vault does not mint the
-mesh's delivery credentials; it provides secrets to modules, and is itself provisioned the ordinary
-way.
-
-**The vault is node-scoped like every provider** ([ADR 0084](0084-which-provider-serves-a-consumer.md)):
-each node its own, selected the same way, so a module's own secret is held by the vault on the
-module's node. **A mesh that wants no vault runs none** — a module requiring no secret needs
-nothing, which is the same test 0031 applied to identity.
-
-## Consequences
-
-The gap that opened this — a generated local secret with no rotation — **closes without new
-machinery**: rotating such a secret is the vault's provisioner remaking a pair credential, the
-operation 13 already specifies. Backup, audit and break-glass gain an owner — the vault module —
-and become things a design specifies rather than absences. The as-is sentence *"rotation is not a
-mesh operation"* is already false for provisioned pairs and, once the vault ships, for local
-secrets too; the as-is document is updated when it does, not before.
-
-What got harder: a break-glass path — recovering a secret when the sealed delivery path is
-unavailable — must not reintroduce a key that one place holds, which is the property
-[ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md) was built to preserve; how
-the vault offers recovery without it is left to the design as an open question. And a module that
-today bakes a password into its own composition must instead require it from the vault — a
-migration taken module by module, not a flag day.
-
-## Amendment — 2026-09-20, before anything shipped
-
-Recorded on the record itself rather than as a supersession, by its decider, on the day it was
-accepted and before any code was merged against the sentences that change. The original text above
-is left as written; this section says what it got wrong and what stands instead.
-
-**What it got wrong.** The decision treated the vault as a provider like the store — optional, and
-one per node — and left the mesh's own root secrets outside it: the store's superuser, the broker's
-administrator, the controller's contexts, sealed to a node key and nothing else. Those are the
-secrets with no rotation and no recovery, and they are the ones a vault exists for. At genesis they
-are not even secret: the foundation raises its store and broker with fixed, well-known credentials
-and carries those into the mesh. Leaving that floor in place gave module secrets an owner and the
-root secrets none.
-
-**What stands instead.**
-
-- **The vault is a foundation module.** It is installed at genesis as part of the foundation
- ([ADR 0078](0078-the-store-and-broker-are-modules.md) is the precedent: a foundation piece is
- still an ordinary module), not assigned later by a mesh that happens to want one. *"A mesh that
- wants no vault runs none"* is withdrawn. A mesh has root secrets, so a mesh has a vault.
-- **One per mesh, on the control-node.** *"The vault is node-scoped like every provider"* is
- withdrawn. Node scoping ([ADR 0084](0084-which-provider-serves-a-consumer.md)) exists because a
- store holds data a consumer is coupled to; the vault holds nothing a consumer is coupled to, and a
- second one would be a second place to lose. Consumers on other nodes reach it as they reach
- identity.
-- **The vault holds the mesh's root secrets under an operator-held key.** The controller mints and
- delivers exactly as before; in addition, every secret a module holds for itself is sealed a second
- time, to an **operator sealing key** whose private half never enters the mesh. The vault keeps
- those operator-sealed copies on its own disk, outside the store, and can hand them out — they are
- ciphertext to everything but the operator. This is the break-glass path the original text left
- open, and it does **not** reintroduce a key one place holds: the mesh holds blobs it cannot open,
- and the operator holds a key with nothing to open until given a blob. Recovery needs both.
-- **Genesis mints real root secrets and seals them to the operator key first**, so the fixed
- credentials the foundation is raised with are replaced before the mesh is handed over.
-
-**Unchanged.** A module's own secret is a `secret` provision the controller mints and the vault
-records; the provisioned-pair path of [ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md)
-is untouched; the vault stores no plaintext, ever.
-
-**What it costs.** An operator key is a thing a person must keep, and a mesh whose operator key is
-lost has root secrets that can be rotated but not recovered — the same standing as today, stated.
-Sealing every own secret twice is a column and a call. Genesis grows a step. A module's
-vault-provided secret (a pair credential) is not yet sealed to the operator key; that is the next
-increment, not this one.
-
-## References
-
-- [ADR 0031](0031-the-control-plane-authenticates-nobody.md) — identity is a module; this is the
- same move for secrets.
-- [ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md) — the provisioning-credential
- path, left unchanged.
-- [ADR 0078](0078-the-store-and-broker-are-modules.md) — the de-specialisation this completes.
-- [ADR 0084](0084-which-provider-serves-a-consumer.md) — the node-scoping the vault obeys.
-- [issue 068](../04-ISSUES/068-secrets-have-no-owning-module/00-report.md) — the gap.
-- [`03-DESIGN/01-to-be/24-the-secrets-vault.md`](../03-DESIGN/01-to-be/24-the-secrets-vault.md) —
- the design. The source mesh's `secret_locate` / `secret_backup` / `secret_verify` /
- `secret_breakglass` subsystem is the prior art it draws on.
diff --git a/02-DECISIONS/0086-a-secret-reaches-a-process-as-a-file.md b/02-DECISIONS/0086-a-secret-reaches-a-process-as-a-file.md
deleted file mode 100644
index 5bf8e90b..00000000
--- a/02-DECISIONS/0086-a-secret-reaches-a-process-as-a-file.md
+++ /dev/null
@@ -1,75 +0,0 @@
----
-topic: building it
-status: accepted
-date: 2026-09-21
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0085-a-secret-is-a-provision.md
----
-
-# 86. A secret reaches a process as a file, and an exception is declared
-
-## Context
-
-The mesh seals a secret to the machine that uses it and discards the plaintext; the host unseals it
-into a file at 0600 ([ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md)). Then
-the module hands it to its container through an env-file, and the runtime puts it where anything
-on the machine that can talk to the runtime can read it: `docker inspect` prints it, and
-`/proc//environ` holds it for the life of the process
-([issue 041](../04-ISSUES/041-a-sealed-credential-ends-up-in-the-process-environment/00-report.md)).
-
-Two facts made this a decision rather than a fix. **It is a supported shape**: placing
-`${secret:…}` in a file a container reads as its environment is what playbook 06 shows, and 36
-containers in 25 catalogue modules do it, the controller's own among them. And **nothing says which
-secrets are exposed**: a reader cannot tell from a manifest whether a credential is protected from
-`inspect` or not, because the manifest looks the same either way. The vault
-([ADR 0085](0085-a-secret-is-a-provision.md)) rests on the seal this undoes.
-
-## Considered Options
-
-1. **Leave it: the environment is where configuration goes.** Rejected — it gives the sealed value
- back to a routine operation, and the care taken to seal it is then theatre.
-2. **Refuse every secret in an environment.** Rejected — some software reads its configuration
- from the environment and nothing else, and a rule the catalogue cannot obey is a rule that gets
- switched off.
-3. **A secret reaches a process as a file; an environment exception is declared, with a reason,
- and refused otherwise.** Adopted.
-
-## Decision
-
-**A secret reaches a process as a file.** A module mounts the file the host wrote and points the
-program at it; the mesh's own programs accept a `_FILE` twin for every variable that carries a
-credential, the way the controller's store connections already did.
-
-**A container that reads a secret from its environment says so.** The manifest key
-`secrets-in-environment` on the container carries the reason. It is catalogue-level — the host
-never sees it — and it exists so a reader can tell from the manifest which secrets are exposed
-that way and why.
-
-**Everything else is refused.** A `${secret:…}` placeholder inside a container's `env` is never
-filled and is refused outright. A file carrying a secret that a container names in `env-file` is
-refused unless the container declares the exception.
-
-## Consequences
-
-The property *a sealed credential is readable only where it is used* becomes a manifest-level
-fact: true where no exception is declared, stated where one is. The controller reads all six of
-its credentials from files; the 35 other containers are marked with their reason and convert one
-by one where their software accepts a path, which is per-module work under
-[issue 041](../04-ISSUES/041-a-sealed-credential-ends-up-in-the-process-environment/00-report.md).
-
-What got harder: a module author meets one more refusal, and the reason they write is only as
-honest as they are. What is not decided: the reach of the exposure on a node — which identities can
-talk to the runtime — which decides whether a declared exception is a hardening item or something
-sharper.
-
-## How it is checked
-
-The catalogue engine refuses at composition, before anything reaches a machine, and its tests
-refuse both shapes and prove the reason is stripped from what is sent.
-
-## References
-
-- [issue 041](../04-ISSUES/041-a-sealed-credential-ends-up-in-the-process-environment/00-report.md)
-- [ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md), [ADR 0085](0085-a-secret-is-a-provision.md)
-- [`03-DESIGN/01-to-be/13-credentials-and-their-rotation.md`](../03-DESIGN/01-to-be/13-credentials-and-their-rotation.md)
diff --git a/02-DECISIONS/0087-a-seeded-file-is-created-once.md b/02-DECISIONS/0087-a-seeded-file-is-created-once.md
deleted file mode 100644
index 0d678921..00000000
--- a/02-DECISIONS/0087-a-seeded-file-is-created-once.md
+++ /dev/null
@@ -1,63 +0,0 @@
----
-topic: what runs on it
-status: accepted
-date: 2026-09-21
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0010-delivery.md
----
-
-# 87. A seeded file is created once, and what grows in it is not the mesh's
-
-## Context
-
-A declaration is complete for what the host owns, and the host reconciles what is declared
-([ADR 0010](0010-delivery.md)): a file with this content, held to it. That is the only thing a
-manifest could say about a file, and it is the wrong thing for a file a module needs to **exist
-before first start** and something else then legitimately writes into — an access list a
-provisioner appends consumers to and the program persists back, a bootstrap configuration a
-program rewrites. Every reconcile restored the seed behind the running program, erased what had
-grown in it, and reported success
-([issue 035](../04-ISSUES/035-reconciling-a-seed-file-wipes-what-grew-in-it/00-report.md)).
-
-A run-once step ([ADR 0052](0052-a-step-that-runs-once-before-a-container.md)) can write a seed
-only if absent, and that closed the instance for a broker whose seed is a program's job. It left
-the general case: a plain file the mesh writes and never overwrites.
-
-## Considered Options
-
-1. **Two owners never share a file: the provisioner owns it, and first-start ordering is solved
- another way.** Rejected as the only answer — some software refuses to start without the file,
- and a module that must ship a program merely to write an empty file has been made to write a
- program to say one word.
-2. **A create-once semantic on a file.** Adopted.
-
-## Decision
-
-A file resource may say `create-once`. The host writes it when it is absent and, when it is
-present, leaves it entirely alone — content, mode and owner — and reports it as **kept**, not
-corrected. What is in the file then is somebody else's work the mesh asked for. The mesh removes
-nothing it did not create ([ADR 0030](0030-data-outlives-the-mesh-that-declared-it.md)); it now
-also does not overwrite what it created once and handed over.
-
-On the security question ADR 0010 asks of every new resource behaviour: this **narrows** what a
-declaration can do to a machine. A create-once file gives a compromised control plane one fewer
-way to change a machine repeatedly — it can seed, once, and never again.
-
-## Consequences
-
-A module says which of its files are seeds, and the difference is visible in the manifest rather
-than in whether the file happened to be revisited. A later change to a seed's declared content
-does not reach a machine that already has the file; that is the meaning of a seed, and a module
-that needs the new content ships it as a run-once step that migrates the existing file.
-
-## How it is checked
-
-The host's apply tests: a seed is created, grown into by hand, reconciled, and the growth survives
-with the outcome `kept`. The vault bed declares one on a real node, grows it, pushes again, and
-reads it back.
-
-## References
-
-- [issue 035](../04-ISSUES/035-reconciling-a-seed-file-wipes-what-grew-in-it/00-report.md)
-- [ADR 0010](0010-delivery.md), [ADR 0030](0030-data-outlives-the-mesh-that-declared-it.md), [ADR 0052](0052-a-step-that-runs-once-before-a-container.md)
diff --git a/02-DECISIONS/0088-the-foundation-filters-before-anything-listens.md b/02-DECISIONS/0088-the-foundation-filters-before-anything-listens.md
deleted file mode 100644
index 311663cd..00000000
--- a/02-DECISIONS/0088-the-foundation-filters-before-anything-listens.md
+++ /dev/null
@@ -1,65 +0,0 @@
----
-topic: the mesh
-status: accepted
-date: 2026-09-21
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0078-the-store-and-broker-are-modules.md
----
-
-# 88. The foundation filters before anything listens
-
-> **Superseded in part — 2026-09-22, by [ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md).**
-> On a node raised adopted, the base ruleset is not loaded: its duty, the store never reachable from outside,
-> passes to a table of the mesh's that only refuses. On every other node the base ruleset, loaded before the
-> store and broker are raised, stands as decided here.
-
-## Context
-
-Adopting the store and broker as ordinary modules
-([ADR 0078](0078-the-store-and-broker-are-modules.md)) needs them reachable by consumers across
-the mesh, so genesis raises them bound to every interface. The packet filter that decides who may
-reach them is a module too, installed a dozen steps later. Between the two, a control-node facing
-the network has its store and its bus open to anyone who can reach the machine, for the length
-of the install ([issue 054](../04-ISSUES/054-the-adopted-store-and-broker-are-open-before-the-filter/00-report.md)).
-The design's rule — what a port is reachable from is decided by the filter — is enforced by
-nothing for that window.
-
-## Considered Options
-
-1. **Accept the window**: the machine is mid-bootstrap and the exposure matches what the
- pre-adoption modules had in steady state. Rejected — the whole point of deriving the filter
- was to stop accepting that.
-2. **Bind narrowly at genesis and widen once the filter exists.** Rejected — a bind change is a
- recreate of the mesh's store during install, and adoption in place needs the same spec.
-3. **The foundation carries a filter of its own, applied before the store.** Adopted.
-
-## Decision
-
-The foundation bundle installs the packet filter and loads a base ruleset **before the store and
-broker are raised**: drop by default; keep loopback, replies, ping and ssh; keep the mesh's own
-ports a node must reach before it is on the private network — the bus it enrols over and the
-registry it pulls from; and let the container runtime's own networks through the forward chain so
-containers keep working. It is written into the **same table** the filter module later derives, so
-that module replaces it wholesale the moment it can compute one from what the mesh knows, and
-nothing of the base survives to contradict it.
-
-## Consequences
-
-From its first resource a machine being made into a mesh refuses what it will refuse when
-finished; the window closes. What got harder: the base ruleset is static and names two ports the
-mesh's derived one also names — a change to which ports the foundation needs is now made in two
-places, and the bundle's own test says which.
-
-## How it is checked
-
-The installer's bundle test asserts the filter and its load precede the store and broker and that
-the rules name ssh, the bus and the registry and not the store's or broker's client ports. The
-genesis bed probes the machine from outside throughout the install: the store's port is never
-reachable, while the bus becomes reachable.
-
-## References
-
-- [issue 054](../04-ISSUES/054-the-adopted-store-and-broker-are-open-before-the-filter/00-report.md), issue 047
-- [ADR 0078](0078-the-store-and-broker-are-modules.md)
-- [`03-DESIGN/01-to-be/07-the-foundation.md`](../03-DESIGN/01-to-be/07-the-foundation.md)
diff --git a/02-DECISIONS/0089-a-bed-reads-the-catalogue-it-proves.md b/02-DECISIONS/0089-a-bed-reads-the-catalogue-it-proves.md
deleted file mode 100644
index 967d8791..00000000
--- a/02-DECISIONS/0089-a-bed-reads-the-catalogue-it-proves.md
+++ /dev/null
@@ -1,69 +0,0 @@
----
-topic: checking it
-status: accepted
-date: 2026-09-21
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0016-the-lab.md
----
-
-# 89. A bed reads the catalogue it proves
-
-## Context
-
-A lab bed installs a catalogue module and asserts what the mesh does with it; "proven in the
-lab" is the standard a module must meet before it ships. The beds built the manifests they
-install inline — a literal copied from the catalogue when each bed was written, and never since.
-Six modules were converted to file-delivered secrets ([ADR 0086](0086-a-secret-reaches-a-process-as-a-file.md))
-and not one bed ran the converted shape; each ran its copy, and the copies still delivered
-secrets the way the catalogue engine now refuses
-([issue 073](../04-ISSUES/073-beds-carry-copies-of-catalogue-manifests/00-report.md)). A run's
-receipt named the commits of the host, the controller and the lab, and said nothing about the
-catalogue, so a catalogue change and a proven catalogue change were indistinguishable.
-
-The end-to-end design already has the rule this breaks — *the run rebuilds what it tests; an
-artifact rebuilt from memory is one rebuilt sometimes* — for binaries and images. A manifest is
-an artifact too.
-
-## Considered Options
-
-1. **Keep the copies and check them against the catalogue** — a test that diffs each literal
- against the module's manifest, ignoring what the lab must rewrite. Rejected: it keeps two
- sources of truth and adds a third thing that can drift, the list of what to ignore.
-2. **Read the catalogue, rewriting only what the lab must.** Adopted.
-
-## Decision
-
-A bed that installs a catalogue module reads that module's manifest from the catalogue checkout
-the run was pointed at. It may rewrite what the lab must and nothing else: a build artifact
-becomes the image the machine holds, an image is pinned to what the machine holds, a host port
-is remapped where one machine carries colliding modules, and an address may point at a stand-in
-the bed raises in place of an upstream. Everything else is the catalogue's, verbatim.
-
-A bed that needs less than the catalogue declares — no upstream server, a secret in the
-environment, a requirement edge removed — is not testing that module. It is a mesh test, and it
-carries a name of its own (see [issue 074](../04-ISSUES/074-a-mesh-test-wears-a-catalogue-modules-name/00-report.md)).
-
-The run's receipt names the catalogue's commit alongside the other repositories', so a receipt
-taken before a manifest changed says so.
-
-## Consequences
-
-A catalogue change is proven by the beds that install the module, or it is not proven, and the
-receipt says which. What got harder: a bed can no longer trim a module to the shape it finds
-convenient; it meets the module's declared requirements or gives its fixture another name.
-The beds that still carry a copy are declared, each with its reason, and the declared list
-only shrinks.
-
-## How it is checked
-
-A unit test in the lab refuses an inline manifest literal that names a catalogue module unless
-the bed is declared, with its reason, in the test's own list; a declaration for a bed that no
-longer carries the copy is refused too, so the list cannot outlive the debt. The receipt test
-asserts the catalogue is claimed whenever the run is pointed at one.
-
-## References
-
-- [issue 073](../04-ISSUES/073-beds-carry-copies-of-catalogue-manifests/00-report.md), [issue 074](../04-ISSUES/074-a-mesh-test-wears-a-catalogue-modules-name/00-report.md)
-- [ADR 0016](0016-the-lab.md), [ADR 0086](0086-a-secret-reaches-a-process-as-a-file.md)
-- [`03-DESIGN/01-to-be/01-end-to-end-testing.md`](../03-DESIGN/01-to-be/01-end-to-end-testing.md)
diff --git a/02-DECISIONS/0090-a-failure-that-repeats-is-said-to-be-stuck.md b/02-DECISIONS/0090-a-failure-that-repeats-is-said-to-be-stuck.md
deleted file mode 100644
index 972086cb..00000000
--- a/02-DECISIONS/0090-a-failure-that-repeats-is-said-to-be-stuck.md
+++ /dev/null
@@ -1,65 +0,0 @@
----
-topic: the mesh
-status: accepted
-date: 2026-09-21
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0010-delivery.md
----
-
-# 90. A failure that repeats is said to be stuck
-
-## Context
-
-Delivery is a comparison, not a one-shot ([ADR 0010](0010-delivery.md)): a node re-applies the
-declaration it holds on a steady interval and reports each time. That is right for a failure that
-goes away by itself — the overlay not up yet, a registry briefly unreachable — and it makes a
-failure that will never go away look exactly the same. A resource nothing can ever apply is
-attempted, fails, is reported, and is attempted again every few minutes, indefinitely; the mesh
-keeps one report per machine, replaced, so each attempt arrives as "failed" at a fresh time.
-Nothing distinguished "failed once, will succeed when its dependency arrives" from "failed
-identically for ever", and nothing escalated the second
-([issue 065](../04-ISSUES/065-a-permanently-failing-resource-is-retried-for-ever-with-no-escalation/00-report.md)).
-
-## Considered Options
-
-1. **The host gives up** after some number of attempts. Rejected: the host does not know whether
- a failure is permanent — that a registry has not answered three times is not evidence it never
- will — and a host that stops trying is a node that must be pushed to again by hand.
-2. **A duration** since the failure was first seen. Rejected as the signal: a laptop shut for a
- week has had one attempt, and a week is not evidence of anything.
-3. **The controller counts identical reports**, and says when there are enough of them. Adopted.
-
-## Decision
-
-The mesh keeps, beside each machine's last report, when the current failure was first reported
-and how many reports in a row have said it — the same outcome, the same refusal, the same failed
-resources by id. Not by the host's words: an error carrying a duration or a counter would read as
-new on every report, and the resource looping on it is exactly what this is for. A report that
-says something different starts the count again; a clean apply clears it. **Three identical reports in a row make a machine stuck**: `status` says
-so beside the failure, with the count and the time it began, and the machine-readable status
-carries the same three facts. The host keeps retrying; being stuck is a statement about the
-mesh's knowledge, not an instruction to the machine.
-
-The controller counts rather than the host, because only it sees every node: one stuck machine
-and a mesh-wide fault are different situations, and the host cannot tell them apart.
-
-## Consequences
-
-A resource that will never apply is visible from `status` after three reconcile intervals, to
-anyone who looks, without being asked for. What got harder: nothing on the machine changes — a
-gating failure that stops what follows still stops it, and this only makes the wait visible.
-Whether a stuck machine should also be raised as an event, and what a gating failure should do,
-stay open in the issue's own questions.
-
-## How it is checked
-
-An inventory test records the same failure three times and asserts the count and the unchanged
-start; the same resource failing in other words, and asserts the count went on; a different
-failure, and asserts it restarted; a clean apply, and asserts both cleared. The status command's own test asserts a stuck machine is said to be one.
-
-## References
-
-- [issue 065](../04-ISSUES/065-a-permanently-failing-resource-is-retried-for-ever-with-no-escalation/00-report.md)
-- [ADR 0010](0010-delivery.md)
-- [`03-DESIGN/01-to-be/10-delivery.md`](../03-DESIGN/01-to-be/10-delivery.md)
diff --git a/02-DECISIONS/0091-a-mount-is-declared-three-ways.md b/02-DECISIONS/0091-a-mount-is-declared-three-ways.md
deleted file mode 100644
index c890497e..00000000
--- a/02-DECISIONS/0091-a-mount-is-declared-three-ways.md
+++ /dev/null
@@ -1,68 +0,0 @@
----
-topic: what runs on it
-status: accepted
-date: 2026-09-21
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0051-shared-data-is-the-operators.md
----
-
-# 91. A mount is declared, and there are three things it can be
-
-## Context
-
-A container's bind mount whose source does not exist is created by the container runtime, as
-root, with whatever mode it picks. So `owner` and `mode` — which exist so a module can say who
-its data belongs to — never reach the directories that hold data, and the rule that keeps a
-directory when a module goes away ([ADR 0030](0030-data-outlives-the-mesh-that-declared-it.md))
-does not cover them, because the mesh has never heard of them
-([issue 026](../04-ISSUES/026-the-data-directories-are-not-declared/00-report.md)). Fourteen
-such mounts were declared by hand; a check that every mount is declared was then written and
-withdrawn, because it refused the builder: the builder mounts the container runtime's socket,
-which is not its data, already exists, and belongs to the machine. Declaring it as the module's
-own directory would be a lie the host would act on.
-
-## Considered Options
-
-1. **Declare the socket as a directory anyway.** Rejected: the host would create, own and
- protect a path that is the machine's.
-2. **A new manifest field** naming machine paths a module may mount. Rejected: the manifest
- already says the module needs the container runtime, and a second field would say the same
- thing in paths.
-3. **Three declarations, one for each kind of path a mount can be.** Adopted.
-
-## Decision
-
-A container may not mount a path the module never declared, and a path is declared in one of
-three ways, which are the three things a path can be:
-
-- **the module's own** — a directory or file resource, or where a secret, a grant or a
- contribution lands. Created and owned by the mesh for this module, kept when the module goes;
-- **the operator's** — an `accesses` entry ([ADR 0051](0051-shared-data-is-the-operators.md)):
- pre-existing, shared, granted for use, never owned;
-- **the machine's** — a facility a declared capability grants. `container-runtime` grants its
- socket. The path exists, the machine owns it, and the capability is the declaration.
-
-A mount under a declared directory is declared. The check runs where the manifest is parsed,
-and names the path and the three remedies.
-
-## Consequences
-
-Every directory that holds a module's data is one the mesh created with the module's owner and
-mode, and one ADR 0030 protects. What got harder: a manifest borrowed from a compose file no
-longer passes on the strength of its volume lines; each must say what kind of path it mounts.
-The table of what a capability grants is small and in the catalogue's parser; a new capability
-that grants a path adds a row.
-
-## How it is checked
-
-Manifest tests refuse an undeclared mount, accept one under a declared directory, accept one
-the module accesses, and accept the runtime's socket with the capability and refuse it without.
-A test parses every manifest in the catalogue beside the checkout and fails on any that breaks
-the rule, so the catalogue cannot drift back.
-
-## References
-
-- [issue 026](../04-ISSUES/026-the-data-directories-are-not-declared/00-report.md)
-- [ADR 0030](0030-data-outlives-the-mesh-that-declared-it.md), [ADR 0051](0051-shared-data-is-the-operators.md)
-- [`03-DESIGN/01-to-be/18-building-a-module.md`](../03-DESIGN/01-to-be/18-building-a-module.md)
diff --git a/02-DECISIONS/0092-an-operator-delivers-a-pair-credential.md b/02-DECISIONS/0092-an-operator-delivers-a-pair-credential.md
deleted file mode 100644
index 7f8890a9..00000000
--- a/02-DECISIONS/0092-an-operator-delivers-a-pair-credential.md
+++ /dev/null
@@ -1,61 +0,0 @@
----
-topic: the tiers
-status: accepted
-date: 2026-09-21
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0085-a-secret-is-a-provision.md
----
-
-# 92. An operator delivers a pair credential, and the mesh never replaces it
-
-## Context
-
-[ADR 0085](0085-a-secret-is-a-provision.md) names three species of secret and gives the vault
-two of them: a module's own secret, which the mesh mints, and an operator-delivered secret — a
-credential for something outside the mesh, which only a person can supply. Under 0085 a secret
-from the vault is a pair credential between the consumer and the vault. The controller's one
-command that takes a value from a person wrote only a module's own secret, sealed to one node.
-Nothing could put a value into a pair, so the third species had no entry
-([issue 070](../04-ISSUES/070-an-operator-cannot-deliver-a-pair-credential/00-report.md)), and
-an operator's credential could be held only as an own secret — un-audited, un-rotatable, the
-gap 0085 opened to close.
-
-## Considered Options
-
-1. **A new verb on the pair.** Rejected: `secret accept` already means "a value a person
- supplied, sealed on the way in, plaintext discarded"; a second verb would mean the same.
-2. **`secret accept` grows a provider end.** Adopted.
-
-## Decision
-
-`secret accept --provider ` seals the supplied value to the
-consumer's node, to the provider's node, and to the operator's key when the mesh has one, and
-records the pair as `accepted`. Every pair credential now says where it came from: `made` or
-`accepted`.
-
-An accepted pair is never replaced by a made one. When a sealing key at either end changes, the
-mesh cannot re-seal a value it does not hold, so the read is refused and names the remedy —
-accept it again. `rotate` refuses an accepted pair for the same reason: the mesh cannot make its
-replacement, and deleting it would have the next read mint one, delivered and reported as
-applied while failing to authenticate somewhere else entirely. Rotating an accepted credential
-is accepting a new value.
-
-## Consequences
-
-A credential for something outside the mesh lives in the vault's ledger with the others, sealed
-to both ends and recoverable by the operator. What got harder: a mesh whose node keys change
-cannot heal an accepted pair by itself; a person is asked. That is the honest shape — the value
-was never the mesh's to make.
-
-## How it is checked
-
-An inventory test accepts a value into a pair, reads it back twice unchanged with origin
-`accepted`, asserts `rotate` refuses it naming the remedy while a made pair still rotates;
-another changes a node's key and asserts the read is refused, then accepts again and reads.
-
-## References
-
-- [issue 070](../04-ISSUES/070-an-operator-cannot-deliver-a-pair-credential/00-report.md), [issue 069](../04-ISSUES/069-one-secret-provision-yields-one-value/00-report.md)
-- [ADR 0085](0085-a-secret-is-a-provision.md)
-- [`03-DESIGN/01-to-be/24-the-secrets-vault.md`](../03-DESIGN/01-to-be/24-the-secrets-vault.md)
diff --git a/02-DECISIONS/0093-a-fixture-that-runs-a-modules-runtime-carries-its-name.md b/02-DECISIONS/0093-a-fixture-that-runs-a-modules-runtime-carries-its-name.md
deleted file mode 100644
index dcc8f938..00000000
--- a/02-DECISIONS/0093-a-fixture-that-runs-a-modules-runtime-carries-its-name.md
+++ /dev/null
@@ -1,53 +0,0 @@
----
-topic: checking it
-status: accepted
-date: 2026-09-21
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0089-a-bed-reads-the-catalogue-it-proves.md
----
-
-# 93. A fixture that runs a module's runtime carries the module's name
-
-## Context
-
-[ADR 0089](0089-a-bed-reads-the-catalogue-it-proves.md) said a bed that needs less than a module
-declares is a mesh test, and carries a name of its own. Twelve beds were to be renamed on that
-basis ([issue 074](../04-ISSUES/074-a-mesh-test-wears-a-catalogue-modules-name/00-report.md)).
-Reading how a module's tools are reached showed why they cannot be: the account the mesh issues
-a module is scoped to `serve..*` from the manifest's name, and the runtime binds its tool
-queues under the name baked into its image. A fixture named otherwise but running the real
-runtime would be refused its own queues. The name is not a label; it is the tool namespace and
-the broker scope.
-
-## Considered Options
-
-1. **Rename anyway, and rebuild each runtime under the fixture's name.** Rejected: a runtime built
- under a false name proves nothing about the module and costs a build per bed.
-2. **A fixture that runs a module's runtime carries the module's name, and therefore reads the
- catalogue.** Adopted.
-
-## Decision
-
-A bed that runs a module's runtime installs the catalogue's manifest for that module and what it
-requires — the vault for a `secret`, the route module for a route — and proves the mechanism
-against the real module. A name of its own is for a fixture that runs no real runtime: a
-declaration-only stub, a bare upstream image.
-
-## Consequences
-
-The mechanism beds become module beds with a mechanism inside them, which is more than they
-were. What got harder: a bed that wanted a cut-down redis now raises the vault beside it; one that
-wanted a sidecar without its server raises the server. Each conversion is a lab run, and the beds
-still carrying a copy are declared with this reason until converted.
-
-## How it is checked
-
-The lab's inline-copy check refuses undeclared copies as before; the declared list's reason for
-these beds names this record. Two beds converted with the vault beside them ran green.
-
-## References
-
-- [issue 074](../04-ISSUES/074-a-mesh-test-wears-a-catalogue-modules-name/00-report.md)
-- [ADR 0089](0089-a-bed-reads-the-catalogue-it-proves.md), [ADR 0047](0047-a-module-runs-its-code-as-its-own-process-with-its-own-account.md)
-- [`03-DESIGN/01-to-be/01-end-to-end-testing.md`](../03-DESIGN/01-to-be/01-end-to-end-testing.md)
diff --git a/02-DECISIONS/0094-a-module-may-hold-several-secrets-from-one-provider.md b/02-DECISIONS/0094-a-module-may-hold-several-secrets-from-one-provider.md
deleted file mode 100644
index 13b515e1..00000000
--- a/02-DECISIONS/0094-a-module-may-hold-several-secrets-from-one-provider.md
+++ /dev/null
@@ -1,63 +0,0 @@
----
-topic: the tiers
-status: accepted
-date: 2026-09-21
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0085-a-secret-is-a-provision.md
----
-
-# 94. A module may hold several secrets from one provider, each a pair of its own
-
-## Context
-
-[ADR 0085](0085-a-secret-is-a-provision.md) makes a module's own secret a provision: the module
-requires `secret` from the vault and reads the pair credential minted for that consumer↔vault
-pair. A pair has one credential, a module requires a provision once, and so a module received
-one value. Read against the catalogue, nine modules hold two or more secrets besides their
-broker account; seven of them hold genuinely independent values with independent lifetimes — a
-root certificate, its key and that key's password; an admin password beside an API token. None
-derives from another, so "one value, derivation the module's business" answers nothing
-([issue 069](../04-ISSUES/069-one-secret-provision-yields-one-value/00-report.md)).
-
-## Considered Options
-
-1. **One value per module; the module derives the rest.** Rejected: the values are independent.
-2. **Require the provision several times.** Rejected: `requires` is a list of names, and a
- requirement is matched by name everywhere.
-3. **The `secrets` map names several files under local names, and each local name is a pair
- credential of its own.** Adopted.
-
-## Decision
-
-A module's `secrets` entry for a requirement may be a path, as before, or an object of local
-names to paths. Each local name is its own pair credential, keyed on it beside the provision,
-the consumer node, the consumer module and the provider; its own file on the consumer, referred
-to as `${secret:}`; its own holder at the provider, named the consumer's identity
-with the local name after it; and rotated apart from the others. A local name may not be one of
-the module's own secrets or something it requires, so what a placeholder means is never
-ambiguous. The plain shape is unchanged, and every credential that exists is the one it was.
-
-The holder's suffix is not a login any backend checks — a secret is not a login — so the
-identity limit that binds a database role or an access key does not apply to it.
-
-## Consequences
-
-The ten modules that could not move onto the vault can. What got harder: `rotate secret` for a
-consumer rotates every local name it holds from that provider; rotating one of several is a
-finer command than the mesh has, and waits for a case that needs it.
-
-## How it is checked
-
-Manifest tests read both shapes, write them back, and refuse a colliding or unusable local
-name. A resolver test asserts two local names are two needs, two files with two credentials,
-and two holders at the provider. An inventory test asserts two local names are two rows, that
-rotating one leaves the other, and that the provider is told both. The vault bed installs a
-consumer that keeps two secrets and asserts two values delivered, two holders in the vault's
-ledger, and both rotated by one command.
-
-## References
-
-- [issue 069](../04-ISSUES/069-one-secret-provision-yields-one-value/00-report.md)
-- [ADR 0085](0085-a-secret-is-a-provision.md), [ADR 0049](0049-a-consumers-identity-fits-the-tightest-backend.md)
-- [`03-DESIGN/01-to-be/24-the-secrets-vault.md`](../03-DESIGN/01-to-be/24-the-secrets-vault.md)
diff --git a/02-DECISIONS/0095-the-control-plane-is-the-way-to-ask-a-module.md b/02-DECISIONS/0095-the-control-plane-is-the-way-to-ask-a-module.md
deleted file mode 100644
index 8eebbec0..00000000
--- a/02-DECISIONS/0095-the-control-plane-is-the-way-to-ask-a-module.md
+++ /dev/null
@@ -1,68 +0,0 @@
----
-topic: the tiers
-status: accepted
-date: 2026-09-21
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0047-a-module-runs-its-code-as-its-own-process-with-its-own-account.md
----
-
-# 95. The control plane is the way to ask a module
-
-## Context
-
-A module serves tools over the broker under an account scoped to what it emits, consumes and
-serves ([ADR 0047](0047-a-module-runs-its-code-as-its-own-process-with-its-own-account.md)). A
-tool call is a request and a reply: the caller creates a reply queue and publishes to the serving
-module's request key, and no module's scope grants either — nor should it, since a module that
-only publishes events has no business declaring queues. So a module could serve tools and nothing
-in the mesh could call them
-([issue 049](../04-ISSUES/049-a-module-can-serve-tools-and-nothing-can-call-them/00-report.md)):
-not an operator at a terminal, not an agent acting for one.
-
-## Considered Options
-
-1. **Calling is a grant**: a module declares it may be asked, and a consumer is issued an
- account that may create a reply queue and publish to that module's request key. Deferred: a
- module-to-module call is the only caller that resembles what the mesh mints today, and none
- asks for one yet.
-2. **The control plane is the way in.** Adopted. It holds a connection that may already, so a
- person or an agent asks through it, and every question passes one process — which is where
- an audit of who asked what belongs.
-
-## Decision
-
-`mesh-controller ask [json]` publishes the request on the tool exchange under
-`.`, with a private reply queue bound under its own name, waits for the answer
-whose correlation matches, and prints it as the module gave it. A tool that answered with an
-error has answered: the answer is printed and the exit status says so. A module that never
-answers is said to have not answered, with where to look.
-
-A module declares nothing about being asked: serving a tool is being askable through the control
-plane. A module-to-module call, if one is wanted, is a grant like any other and a later decision.
-
-## Consequences
-
-Anything with the control plane in reach can ask any module anything it serves. What got
-harder: nothing outside the control plane can, and the control plane's connection is one more
-thing on the path of every question — a cost accepted for the audit it buys.
-
-> **The mechanism changed — 2026-09-30, by ADR 0152.** What stands: `ask` on the control plane, and
-> that every call passes an account whose permission list says what it may ask. What moved: "nothing
-> outside the control plane can" stopped being true when a person's account gained a publish grant per
-> tool (design 25 §7, 2026-09-28), and [ADR 0152](0152-the-operators-surface-is-a-module-the-console.md)
-> takes the first option above for a module as well — a manifest declares `invokes`, and the bus grants
-> exactly that publish side. The audit the second option bought is the bus's permission list, which
-> derives both.
-
-## How it is checked
-
-A tools-only bed asks a served tool through the control plane and asserts an answer arrived —
-an error, since the lab has no upstream and no token, which is an answer where a timeout would
-not be.
-
-## References
-
-- [issue 049](../04-ISSUES/049-a-module-can-serve-tools-and-nothing-can-call-them/00-report.md)
-- [ADR 0047](0047-a-module-runs-its-code-as-its-own-process-with-its-own-account.md)
-- [`03-DESIGN/01-to-be/19-the-module-protocol.md`](../03-DESIGN/01-to-be/19-the-module-protocol.md)
diff --git a/02-DECISIONS/0096-an-upstream-image-is-copied-between-registries.md b/02-DECISIONS/0096-an-upstream-image-is-copied-between-registries.md
deleted file mode 100644
index 69265d6c..00000000
--- a/02-DECISIONS/0096-an-upstream-image-is-copied-between-registries.md
+++ /dev/null
@@ -1,66 +0,0 @@
----
-topic: building it
-status: accepted
-date: 2026-09-21
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0006-the-substrate-and-the-control-plane.md
----
-
-# 96. An upstream image is copied between registries, never through a machine's image store
-
-## Context
-
-A module may declare that an artifact is an image published elsewhere, to be copied into the
-mesh's own registry so machines fetch it by a digest this mesh assigned rather than by a name
-somebody else controls. The builder pulled it into the build machine's image store and pushed it
-under the mesh's name, and the push was refused: a published image is an index over several
-architectures, the runtime's store keeps the index, and pushing one platform out of it fails
-however the platform is asked for
-([issue 046](../04-ISSUES/046-an-upstream-image-cannot-be-mirrored-into-the-mesh/00-report.md)).
-Every variant of pull-then-push was tried and failed the same way.
-
-## Considered Options
-
-1. **Resolve the index to one platform and push that.** Tried, reverted: it did not make the
- push work, and a workaround for a store's behaviour is a thing nobody removes later.
-2. **Tooling that copies between registries**, installed on the build machine. Rejected: one
- more thing the builder's image carries, for a protocol the builder already speaks for blobs.
-3. **Copy over the registry API**, in the builder. Adopted.
-
-## Decision
-
-The builder copies an upstream image between registries and never through a machine's image
-store: it reads the index and every manifest it names, moves each blob by digest into the mesh's
-registry — skipping what is already there, since blobs are content-named — puts the manifests
-and then the index under the module's repository, and pins the index's digest. Public images are
-read with the anonymous bearer token the registry hands out on challenge, which is how the
-public hub and the others the catalogue names serve them. The mesh mirrors the whole index, so
-what a machine fetches is the image for its own architecture; that every machine on one mesh is
-the same architecture is an assumption this mesh makes and had not written down until now.
-
-Genesis has no registry to copy into and keeps the pull: the image stays in the first machine's
-store, named by its own id, as every artifact does before there is anywhere to publish.
-
-## Consequences
-
-An upstream artifact builds. What got harder: the builder now holds a registry client of its
-own, some two hundred lines, where a runtime command used to do; and a private upstream that
-demands a credential is refused, since the copy is anonymous by design.
-
-## How it is checked
-
-A test raises a fake upstream registry serving an index over two platforms behind a bearer
-challenge, and a fake mesh registry that records what arrives: every blob of both platforms
-arrives once, two manifests and the index are put under their digests, the reference returned
-pins the index under the module's repository, and a second copy uploads nothing. A reference
-test reads names the way a runtime does. *Proven against the real thing the same day:* the genesis
-bed built the tool runtime through the mesh's builder with its node base copied out of the public
-hub into the mesh's registry by this code — after one finding the fake could not give: the builder
-ran on the default bridge, where loopback is not the machine, and now runs on the host network.
-
-## References
-
-- [issue 046](../04-ISSUES/046-an-upstream-image-cannot-be-mirrored-into-the-mesh/00-report.md)
-- [ADR 0006](0006-the-substrate-and-the-control-plane.md)
-- [`03-DESIGN/01-to-be/18-building-a-module.md`](../03-DESIGN/01-to-be/18-building-a-module.md)
diff --git a/02-DECISIONS/0097-a-vendor-image-is-a-declared-build-input.md b/02-DECISIONS/0097-a-vendor-image-is-a-declared-build-input.md
deleted file mode 100644
index f77de354..00000000
--- a/02-DECISIONS/0097-a-vendor-image-is-a-declared-build-input.md
+++ /dev/null
@@ -1,76 +0,0 @@
----
-topic: building it
-status: accepted
-date: 2026-09-21
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0096-an-upstream-image-is-copied-between-registries.md
----
-
-# 97. A vendor image is a declared build input, and a recipe fetches nothing undeclared
-
-> **Superseded in part — 2026-10-08, by [ADR 0257](0257-a-copied-base-is-recorded-where-it-is-copied-kept-by-the-builds-that-stood-on-it-and-held-once-per-image.md).**
-> A declared image is no longer copied "under the module's repository": it is copied into one repository per
-> upstream image, whichever modules stand on it. Everything else decided here stands.
-
-## Context
-
-Three modules could not be built by the mesh's builder because their recipes reached for what
-no manifest named: a public package, or a binary copied out of a public image
-([issue 064](../04-ISSUES/064-a-mesh-build-cannot-fetch-a-modules-external-dependencies/00-report.md)).
-A module already names the bases it stands on — another module's artifact, by name — and the
-builder answers with what the mesh holds; a vendor's image had no such declaration, so a recipe
-named it directly and the build worked when the public registry answered, which is sometimes.
-
-## Considered Options
-
-1. **Let the build environment reach public registries.** Rejected: a build that fetches from
- somebody else's registry on its own is one the mesh cannot rebuild the same way twice.
-2. **A vendor image is a base like any other**, declared under `build.on` with the argument the
- recipe reads it from, pinned by digest, copied into the mesh's registry before the build
- ([ADR 0096](0096-an-upstream-image-is-copied-between-registries.md)). Adopted.
-
-## Decision
-
-A build's `on` entry is either a module's artifact or an image published elsewhere, pinned by
-digest, read from one build argument. Before the build the image is copied into the mesh's
-registry under the module's repository and the recipe is handed the copy; genesis, with no
-registry, pulls it into the first machine's store. A recipe whose `COPY --from` names a registry
-image the manifest did not declare is refused before the build, naming the image and the remedy;
-its own stages, declared arguments and `scratch` are not fetches. An unpinned vendor image is
-refused: a tag is what somebody else can move.
-
-> **The mechanism changed — 2026-10-08, by [ADR 0257](0257-a-copied-base-is-recorded-where-it-is-copied-kept-by-the-builds-that-stood-on-it-and-held-once-per-image.md).**
-> A declared image is still copied into the mesh's registry before the build, and the recipe is still handed
-> the copy. What moved is where the copy goes. It no longer goes "under the module's repository"
-> (`/on-`). It goes into one repository per upstream image, `upstream//`,
-> whichever modules stand on it. Each copy is also recorded where it is made, so the store's sweep can
-> let it go.
-
-A recipe whose `FROM` names an undeclared base was at first said, not refused: the mesh's own
-images — the control plane's, the tool runtime's, the route proxy's — started from a public base
-and declared none, and refusing those refuses genesis. *Amended the same day:* those three declare
-their bases now, and an undeclared `FROM` is refused like an undeclared copy. The builder's own
-image and the examples are built by `make`, not by the mesh, and take arguments with defaults.
-
-The package half of the issue is not decided here: the mesh's package registry already proxies
-the public one, and the failure the report saw has to be run again to be placed.
-
-## Consequences
-
-A module's build inputs are all in its manifest, and every one of them is something the mesh
-holds a copy of. What got harder: a recipe that used to name a base image on its first line now
-names an argument, and the manifest names the image.
-
-## How it is checked
-
-A builder test declares a pinned vendor image, asserts it is copied under the module's
-repository and handed to the recipe as the argument, and asserts an unpinned one is refused. A
-recipe test asserts an undeclared `FROM`, an undeclared `COPY --from` and an undeclared argument
-are named, and that stages, declared arguments and `scratch` are not.
-
-## References
-
-- [issue 064](../04-ISSUES/064-a-mesh-build-cannot-fetch-a-modules-external-dependencies/00-report.md)
-- [ADR 0096](0096-an-upstream-image-is-copied-between-registries.md)
-- [`03-DESIGN/01-to-be/18-building-a-module.md`](../03-DESIGN/01-to-be/18-building-a-module.md)
diff --git a/02-DECISIONS/0098-a-fact-a-provider-makes-at-first-start-is-fetched-from-it.md b/02-DECISIONS/0098-a-fact-a-provider-makes-at-first-start-is-fetched-from-it.md
deleted file mode 100644
index bad28718..00000000
--- a/02-DECISIONS/0098-a-fact-a-provider-makes-at-first-start-is-fetched-from-it.md
+++ /dev/null
@@ -1,61 +0,0 @@
----
-topic: the tiers
-status: accepted
-date: 2026-09-21
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0085-a-secret-is-a-provision.md
----
-
-# 98. A fact a provider makes at first start is fetched from it, not carried in its manifest
-
-## Context
-
-The catalogue's certificate authority declared its root certificate, its root key and that key's
-password as its own secrets, and told the container to initialise from them. The mesh mints an
-own secret nobody delivers as random bytes, and random bytes are not a certificate: issued, the
-authority could not start; only an operator hand-making its root could raise it
-([issue 076](../04-ISSUES/076-a-served-fact-made-at-first-start-cannot-be-served/00-report.md)).
-The authority can make its own root at first start. What it could not do then was tell the mesh
-what that root is: a consumer was given `${bound:acme-ca:root}` from the provider's `serves`,
-which is written in the manifest before anything runs.
-
-## Considered Options
-
-1. **A secret the module makes**, with the mesh taking custody once the file exists. Rejected
- for now: a node would have to send a value up to the mesh, which no channel does today, and
- a root key is the one thing the mesh has no reason to hold.
-2. **A served fact the provider contributes at run time.** Rejected for now: the same new
- channel, for a fact that is not secret at all.
-3. **The consumer fetches it from the provider**, over the mesh network, through a gate before
- the thing that needs it starts. Adopted.
-
-## Decision
-
-A provider's `serves` names where a fact made at first start can be fetched — the authority
-serves its root at a path beside its ACME directory — and a consumer fetches it in a `run-once`
-step declared before the resource that needs it, from the provider's bound address. The mesh
-network is where the fetch happens, which is what makes fetching without a prior trust
-acceptable: it is the network the mesh itself authenticates. The mesh mints only what it can
-make: the authority's password. The root key stays where it was made.
-
-## Consequences
-
-The catalogue's authority starts, and the proxy that requires it trusts what it fetched. What
-got harder: a consumer of such a fact carries one more resource, the gate that fetches it, and
-a fact that changes after first start is refetched only when the declaration changes.
-
-## How it is checked
-
-The route-forwarding bed installs the authority, the proxy and a consumer from the catalogue and
-asserts a routed name is served through the proxy. The proxy refuses to start on a bundle that is
-not a certificate, so the name being served proves the gate fetched one; the gate itself refuses
-a body that is not a certificate. That the proxy obtains a certificate from this authority through
-that root is the certificate bed's proof, against the same authority with the same proxy. The
-catalogue-wide manifest test parses both manifests.
-
-## References
-
-- [issue 076](../04-ISSUES/076-a-served-fact-made-at-first-start-cannot-be-served/00-report.md)
-- [ADR 0053](0053-a-step-that-runs-on-a-schedule.md), [ADR 0085](0085-a-secret-is-a-provision.md)
-- [`03-DESIGN/01-to-be/08-connectivity.md`](../03-DESIGN/01-to-be/08-connectivity.md)
diff --git a/02-DECISIONS/0099-a-step-that-runs-once-names-what-it-reads.md b/02-DECISIONS/0099-a-step-that-runs-once-names-what-it-reads.md
deleted file mode 100644
index e076bcda..00000000
--- a/02-DECISIONS/0099-a-step-that-runs-once-names-what-it-reads.md
+++ /dev/null
@@ -1,94 +0,0 @@
----
-topic: what runs on it
-status: accepted
-date: 2026-09-21
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0052-a-step-that-runs-once-before-a-container.md
----
-
-# 99. A step that runs once names what it reads, and runs again when it changed
-
-## Context
-
-[ADR 0098](0098-a-fact-a-provider-makes-at-first-start-is-fetched-from-it.md) has a consumer
-fetch a fact its provider made at first start through a run-once step: the route proxy fetches
-the certificate authority's root before it starts. A run-once step runs once per declaration
-([ADR 0052](0052-a-step-that-runs-once-before-a-container.md)): its marker is the digest of its
-own declaration, and a re-apply that finds the marker does nothing.
-
-The provider can move. When the authority is assigned to another node it makes a new root there,
-and the mesh rewrites the consumer's binding file with the new address — but the step's own
-declaration has not changed, so the step does not run again, the proxy keeps the old root, and it
-refuses every certificate the new authority issues
-([issue 077](../04-ISSUES/077-a-fact-fetched-at-first-start-is-fetched-once/00-report.md)). A
-restart trigger was the natural remedy and was refused on a run-once step, on the ground that a
-step does not stay running to be restarted.
-
-Two things the host already does point at the answer. What a container reads is part of what it
-is: a container's digest includes the digest of every resource it names under `restart-on`, so a
-rewritten file it reads is a changed container
-([issue 045](../04-ISSUES/045-a-container-keeps-the-values-it-started-with/00-report.md)).
-And a run-once step's marker *is* its digest. Nothing new is needed for the step to run again when
-what it reads changed; only the refusal stands in the way.
-
-## Decision
-
-**A run-once step may name what it reads under `restart-on`. For a step the word means *run
-again*: when a named resource changed in this apply, the step's digest has moved, its marker no
-longer matches, and it runs again — gating what follows, as it did the first time.** Nothing
-about the marker changes; the refusal of the pair is lifted, in the control plane and on the host.
-
-**The container that consumes what a step made names the step.** A step that ran counts as a
-change, so a service that names it under `restart-on` is recreated after it, holding what the step
-fetched. Without this the step fetches a new root and the service keeps serving with the old one.
-
-The route proxy's gate names the binding file it reads; the proxy's server names the gate and the
-binding. When the authority moves, the binding is rewritten, the gate fetches the new root, and the
-server is recreated with it — in one apply.
-
-## Considered Options
-
-1. **A provider epoch in the binding — the mesh raises a number when a provider is re-issued or
- moved, and the consumer's file carries it.** Rejected: the binding already changes when the
- provider moves (its address does), and a re-issue does not change what the authority serves —
- its state persists. An epoch would be a second signal for a change the file already shows.
-2. **The step runs before every start of the service, with no marker.** Rejected: every reconcile
- would run it, and a step that runs on every apply reads as a change on every apply, so the
- service naming it would be recreated every few minutes.
-3. **The proxy fetches the root itself, at start.** Rejected as the general answer: it fixes the
- proxy and leaves the next consumer of a fact made at first start to fix itself. The step is the
- general shape ([ADR 0098](0098-a-fact-a-provider-makes-at-first-start-is-fetched-from-it.md)).
-4. **Lift the refusal and read `restart-on` as *again* on a step.** Adopted: it is what the
- digest already does, and it needs no new word.
-
-## Consequences
-
-A fact fetched at first start follows its provider when the provider moves. What is not covered:
-a provider whose state is wiped behind the mesh's back, on the same node, makes a new fact that
-nothing the mesh knows reflects. That is not a change the mesh can see, and it is not claimed.
-
-The one contradiction the refusal named is real and is now a documented reading: on a running
-container `restart-on` means recreate, on a step it means run again. Both are "this must reflect
-what it reads". This record is about a run-once *container*, the step ADR 0052 defined. A
-run-once process is a different shape whose marker does not carry what it reads; it is not
-covered here.
-
-## How it is checked
-
-- mesh-host: a unit test declares a run-once step naming a file, records its marker against the
- file's old content, applies with the new content and asserts the step ran; applies again with
- nothing changed and asserts it did not. A second test declares a container naming a run-once
- step and asserts the container is recreated after the step ran, with the step as the stated
- reason.
-- mesh-controller: the manifest parser accepts a run-once step with `restart-on`; the
- catalogue-wide manifest test parses the route proxy's manifest, whose gate and server name what
- they read.
-- The route-forwarding bed still passes with the host that accepts the pair. No bed moves the
- authority: the mechanism is proven by the unit tests, the declaration by the manifest test.
-
-## References
-
-- [issue 077](../04-ISSUES/077-a-fact-fetched-at-first-start-is-fetched-once/00-report.md)
-- [ADR 0052](0052-a-step-that-runs-once-before-a-container.md), [ADR 0053](0053-a-step-that-runs-on-a-schedule.md), [ADR 0098](0098-a-fact-a-provider-makes-at-first-start-is-fetched-from-it.md)
-- [`03-DESIGN/01-to-be/08-connectivity.md`](../03-DESIGN/01-to-be/08-connectivity.md), [`03-DESIGN/01-to-be/20-writing-a-module.md`](../03-DESIGN/01-to-be/20-writing-a-module.md)
diff --git a/02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md b/02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md
deleted file mode 100644
index e0cbd990..00000000
--- a/02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md
+++ /dev/null
@@ -1,228 +0,0 @@
----
-topic: the mesh
-status: accepted
-date: 2026-09-22
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0078-the-store-and-broker-are-modules.md
-supersedes-in-part:
- - 02-DECISIONS/0088-the-foundation-filters-before-anything-listens.md
----
-
-# 100. A node in use is adopted before it is converged
-
-> **Superseded in part — 2026-09-23, by [ADR 0105](0105-the-mesh-adopts-the-predecessors-tunnel-in-place.md).**
-> The rule that the private network's range must not overlap a tunnel the predecessor still runs now applies
-> only where the found tunnel is left running beside the mesh's. On the hub, where the mesh adopts the found
-> tunnel in place, there is one tunnel and one range. Everything else decided here stands.
-
-## Context
-
-The mesh replaces a predecessor mesh that is running, on the same machines, with the services
-people use. The control-node is the machine that already carries the predecessor's broker and
-build pipeline. The operator proposed the migration's shape: stop the predecessor's control on a
-machine, bring the mesh up there in an adoption mode that keeps the machine's configuration in
-force, migrate its modules one at a time, move to the next machine, and flip adoption off when every
-machine is done.
-
-Measured on the control-node ([research 012,
-*migrating a node that is in use*](../01-RESEARCH/012-the-minimum-viable-node/migrating-a-node-in-use.md)):
-60 predecessor containers; the predecessor's control in user-level daemons separate from its
-services; its firewall active, with 54 incoming and 52 forwarding rules allowing each served port
-explicitly; 12 files its configuration sync writes, 3 of them system files.
-
-Four things in the mesh as it stands break that shape:
-
-1. **The base ruleset closes the machine.** Genesis loads a drop-by-default table
- ([ADR 0088](0088-the-foundation-filters-before-anything-listens.md)). Every base chain at a hook
- runs in priority order; an accept ends only its own chain and a drop in any is final — whether
- the other firewall's chains are nftables or legacy iptables. So the predecessor's allowed ports
- would close at genesis.
-2. **The host replaces what it finds.** A declared file is written whatever is at its path, except
- a file declared create-once. A declared container replaces a running one of the same name. So
- assigning a module the predecessor also runs replaces the predecessor's service and its files at
- once.
-3. **Some foundation ports are held.** The foundation's store and bus ports are free — the
- predecessor publishes its own elsewhere — but the registry's port and the broker's management
- port are held, and on a control-node that is the private network's hub, so is the private
- network's port. The foundation's ports are fixed in the installer's bundle and the catalogue's
- manifests, so a collision surfaces as a container that fails to bind, and a port changed at
- genesis would be changed back when the foundation is adopted as modules.
-4. **Published container ports are forwarded, not received.** The foundation publishes its ports
- on every interface; a predecessor firewall that filters only incoming traffic never sees them.
- The base ruleset is what keeps the store unreachable from outside, and it is the thing that
- cannot be loaded.
-
-The mesh already adopts in one place: the foundation's store and broker are taken over in place as
-modules, keyed on the container that is already running
-([ADR 0078](0078-the-store-and-broker-are-modules.md)). The research this record draws on already
-settled the conflict rule for adoption: *on conflict, what is on the machine stays*.
-
-## Considered Options
-
-1. **Cut each machine over in one go** — stop the predecessor's store, broker, registry and proxy,
- raise the foundation in their place. Rejected: every predecessor service on the machine is down
- until it has migrated, the predecessor's other machines lose their broker, and the rollback is
- restarting the predecessor — a recovery, not a step.
-2. **Put the control-node on a separate machine.** Rejected: the control-node is decided.
-3. **Converge on joining, as today.** Rejected: the base ruleset closes the machine at genesis, and
- the predecessor's services and files are replaced as soon as any module naming them is assigned.
-4. **Only make the foundation's ports configurable.** Rejected as insufficient: it answers the bind
- collisions and neither the firewall nor the files.
-5. **Treat assigning a module as migrating it.** Considered and rejected on review: it makes the
- rule that keeps found files never fire — the host only ever sees files of assigned modules — and
- it makes an assignment on an adopted node an outage rather than a preparation.
-6. **Adoption as a mode per node; each module taken explicitly; the node converged by an explicit
- flip.** Adopted.
-
-## Decision
-
-**A node is adopted or converged, and the controller records which.** The operator says so: at
-genesis for the control-node, and in the enrolment token for the others. The controller is
-authoritative, and every declaration it sends says whether the node is adopted and which modules
-have been taken on it. A node stays adopted until the operator converges it. An adopted node is
-said to be adopted wherever the mesh reports a node's state.
-
-**A converged genesis refuses a machine in use.** A machine is in use when a container is running
-on it, or a port is listening on an address other than loopback that is not ssh's. Raised without
-saying adopted on such a machine, genesis refuses and names every container and listener it
-counted — a forgotten flag must not close a working machine.
-
-**Before a node is adopted, its predecessor's control is stopped by the operator** — the daemons
-that write its configuration. Its services keep running on what they have.
-
-**Found means present with no record.** A file at a declared path, or a container at a declared
-name, that the host's store has no record of writing is *found*. A file the host wrote in an
-earlier life of the node is not found; its record says so.
-
-**On an adopted node, what is found is kept until its module is taken.** The host keeps a found
-file and a found container as they are, records the file's original content before anything else
-happens to it, and reports each as held. Assigning a module on an adopted node prepares it: what
-the module declares that is not found is created; what is found is held. **Taking a module** on a
-node is its cutover — the operator's act, done when that module's data has moved — and from then
-on the module's resources converge on that node like any other. What is held is never removed,
-even when its module is unassigned, and a held file or container that changes while held — a file
-rewritten, a container stopped or replaced — is reported as changed by something else, not reverted
-or restarted: that is how a predecessor still writing is caught.
-
-**The firewall found on the machine stays in force.** The mesh loads no table on an adopted node
-that drops by default or holds an accept — neither genesis's base ruleset nor the filter module's
-derived one. What the mesh needs reachable is declared as **openings**: a resource that says a port
-is reachable, from where, on the incoming path or the forwarded path — a published container port is
-forwarded. The controller derives them from the same inputs as the filter, each from where the
-filter would admit it: the `listens` of the modules assigned there, the private network's hub port
-and the bus and the registry from anywhere, the store's port and the broker's management port from
-the private network. The host
-converges an opening through the found firewall in that firewall's own terms, marks it as the
-mesh's, and removes only what it marked; it re-checks each opening on every reconcile, so a reload
-or a reboot of the found firewall does not lose it for longer than one reconcile. An opening is a
-state, not a command, which is what lets it travel over the link. The host reports which firewall
-it found. A machine with no firewall needs no openings; a machine with a kind no host speaks is
-refused adoption.
-
-**The mesh guards its own ports itself, in a table of its own that only refuses.** It passes
-everything by default and holds nothing but refusals, and the two ports it refuses are the
-foundation's own, checked free at genesis, so it cannot close anything the machine serves; it is
-the mesh's, so the found firewall reloading does not touch it. It refuses the store's port and the
-broker's management port except from the private network and from the machine itself — its
-loopback and the container runtime's own networks, known by the interface a packet arrives on and
-never by its source address alone — at the prerouting hook, ahead of the runtime's
-destination translation, so it matches the port the packet was sent to, for both address
-families. The bus and the registry stay reachable from anywhere, as a node
-enrols over the bus and pulls from the registry before it has a private-network address
-([ADR 0088](0088-the-foundation-filters-before-anything-listens.md)); so does the private network's
-hub port. The store is unreachable from outside whatever the found firewall does, and on a machine
-with none.
-
-**The foundation's ports are the node's.** Every port the foundation binds is an input to genesis,
-checked free before anything is raised, refused with the name of what holds it. The ports given
-become that node's settings for the foundation's modules — the catalogue's numbers are only their
-defaults — and every place that uses them reads them from there: the modules' containers, the
-filter, the base ruleset, the private network's endpoint, and the addresses consumers are given.
-The private network's address range must not overlap a tunnel the predecessor still runs; genesis
-checks that too.
-
-**Converging a node is one act, previewed.** It refuses while an assigned module still holds a
-found container: each service is taken on its own, when its data has moved, never by the flip. The
-preview lists what is reachable on the machine now — every listening socket and every published
-container port — and for each whether an assigned module declares it or it will close, and every
-module the flip will take, with the held files each will replace. The flip then takes those modules, loads the
-mesh's derived filter in place of its refusal-only table, and retires the found firewall by
-disabling it, never by flushing: the container runtime's rules and the found firewall's own
-configuration stay on disk. Returning a converged node to adopted unloads the derived filter,
-restores the refusal-only table, enables the found firewall again and converges the openings
-through it once more; what was taken stays taken. A
-node converges when its migration is done; the mesh is migrated when every node has converged.
-
-**The order is the operator's:** the control-node first, adopted, its modules assigned and taken
-one at a time; then each other machine, adopted, migrated, converged in turn. The predecessor's
-pipeline runs on the control-node, so its updates stop for every machine while the migration runs;
-that is accepted.
-
-## Consequences
-
-Each step says what it changes before it changes it. Adopting a node changes nothing that serves;
-assigning a module adds what is not there; taking a module replaces one service; the flip replaces
-the firewall, after naming every port it will close. Two steps are not undone by the mesh: taking a
-module replaces the predecessor's container, and the kept original of a file is recorded but not
-yet restored by any act of the mesh
-([research 012](../01-RESEARCH/012-the-minimum-viable-node/00-overview.md) leaves where it lives
-open).
-
-What got harder:
-
-- **The mesh must speak a firewall it did not install**, on both the incoming and the forwarded
- path. One kind is found on the machines measured; another is refused until a host speaks it. The
- mesh's own guard does not depend on it: that table is the mesh's.
-- **The host gains a guard it did not have** — keep what you found — and its report must say
- which files and containers it holds, or an adopted node reads as converged.
-- **The declaration gains a node's mode and its taken modules**, and a resource, the opening.
-- **Genesis grows inputs, and they outlive genesis.** The foundation's ports stop being constants;
- every reader of them reads the node's settings.
-- **A node can sit adopted indefinitely.** Nothing forces the flip; the mesh's status says which
- nodes are adopted, so one left behind is visible.
-- **This narrows [ADR 0088](0088-the-foundation-filters-before-anything-listens.md) for adopted
- nodes**: the base ruleset is not loaded on a node raised adopted, and its duty — the store never
- reachable from outside — passes to a table of the mesh's that only refuses.
-
-## How it is checked
-
-A lab bed prepares a machine the way the predecessor leaves one: its firewall allowing a served
-port and denying the rest, a service container listening on that port under a name a catalogue
-module also uses, a file at a path that module declares, a stand-in for the predecessor's control
-that would rewrite that file, and a container holding the registry's port. Then:
-
-- **Genesis converged** on it refuses and names every container and listener it counted.
-- **Genesis adopted, with the registry's port held**, refuses and names the holder; with another
- port given, the foundation comes up — and adopting the foundation as modules leaves it on that
- port.
-- **Nothing that serves changed**: the service is reachable from a second machine, the file is byte
- for byte what it was, and the found firewall's rules differ only by rules marked as the mesh's.
-- **The store is unreachable from outside** — probed from a machine off the private network, and
- again after the found firewall is reloaded — and reachable over it and from a container on the
- node itself; the bus is reachable from a machine that has not yet enrolled.
-- **The mesh works through the found firewall, and keeps working after it is reloaded and after the
- machine reboots**: the second machine enrols, and the openings are there again.
-- **A predecessor still writing is caught**: with the stand-in left running, the held file's change
- is reported and not reverted.
-- **Assigning prepares, taking cuts over**: the module assigned holds the found container and file;
- taken, it replaces them and its port is opened.
-- **Converging previews, then changes**: it refuses while the service's module holds its found
- container; once that module is taken, the preview names the service's port and a published port no
- firewall rule mentions, and the modules it will take; after the flip the mesh's derived filter is
- loaded, the found firewall is disabled with its configuration still on disk, the declared port is
- open and the undeclared one closed. Returned to adopted, the found firewall is enabled again and
- the derived filter is gone.
-
-Unit tests hold the host to keeping a found file and container on an adopted node, converging them
-once taken, never removing what it holds, and reporting a held file or container that changed;
-genesis to refusing a held port and a converged raise on a machine in use; the controller to
-carrying the mode and the taken modules in every declaration, deriving the openings, and refusing
-a flip while a found container is held.
-
-## References
-
-- [research 012 — the minimum viable node, and adopting what is already there](../01-RESEARCH/012-the-minimum-viable-node/00-overview.md),
- and its document [*migrating a node that is in use*](../01-RESEARCH/012-the-minimum-viable-node/migrating-a-node-in-use.md)
-- [ADR 0005](0005-the-node-host.md), [ADR 0011](0011-managed-files-are-generated-never-edited.md),
- [ADR 0078](0078-the-store-and-broker-are-modules.md), [ADR 0088](0088-the-foundation-filters-before-anything-listens.md)
diff --git a/02-DECISIONS/0101-a-machines-own-resolver-does-not-make-it-in-use.md b/02-DECISIONS/0101-a-machines-own-resolver-does-not-make-it-in-use.md
deleted file mode 100644
index 79b50c60..00000000
--- a/02-DECISIONS/0101-a-machines-own-resolver-does-not-make-it-in-use.md
+++ /dev/null
@@ -1,70 +0,0 @@
----
-topic: the mesh
-status: accepted
-date: 2026-09-22
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md
----
-
-# 101. A machine's own resolver does not make it in use
-
-## Context
-
-[ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md) has a converged genesis
-refuse a machine in use. It defines *in use* as a container running, or a port listening on an
-address other than loopback that is not ssh's. That definition was written before anything was
-measured.
-
-Measured on a freshly installed lab machine, running nothing but its operating system:
-
-| Listening | Held by |
-|---|---|
-| TCP and UDP on every address, the link-local name resolution port | the system's name resolver |
-| UDP on every address, the multicast name resolution port | the system's name resolver |
-| UDP on a link-local address, the address-configuration client port | the system's network manager |
-| TCP and UDP on loopback | the resolver's stub and the container runtime |
-
-By ADR 0100's words, the resolver's TCP listener on every address makes **every** freshly
-installed machine a machine in use. A converged genesis would refuse them all, and `--adopted`
-would become the only way to raise anything. The refusal exists to catch a forgotten flag on a
-working machine. Refusing an empty one defeats it, and teaches operators to pass the flag by
-habit.
-
-## Considered Options
-
-1. **Keep the words.** Rejected: every fresh machine is refused.
-2. **Count TCP only, ignore UDP.** Rejected: the resolver listens on TCP too, and a machine that
- serves over UDP alone, a resolver or a tunnel, is in use.
-3. **Ignore listeners held by the operating system's own network daemons**, a short named list,
- on both protocols. Adopted.
-
-## Decision
-
-**A listener held by one of the operating system's own network daemons does not make a machine
-in use.** The daemons are the ones the measurement found: the name resolver and the network
-manager, named in the installer's code beside that measurement. Everything else in ADR 0100's definition stands: a running container, or any other
-listener on an address other than loopback that is not ssh's, makes the machine in use, and
-genesis still names every one it counted.
-
-A daemon is added to the list only with a measurement of a fresh machine that holds it.
-
-## Consequences
-
-- A converged genesis on a fresh machine goes ahead, as it did before ADR 0100.
-- A machine whose resolver is also serving other machines is not counted as in use by its
- resolver alone. It is one of the daemons that serves nobody on a fresh machine, and the one
- kind of service this lets through.
-- The list is code, not configuration, so it changes by review.
-
-## How it is checked
-
-A unit test in the installer feeds the listeners captured from the fresh machine, as the
-listening-socket tool printed them, and asserts the machine is not in use. The existing tests
-still assert that a serving machine is in use and that every container and listener is named.
-The adoption lab bed raises a converged genesis on a fresh machine and asserts it is not refused.
-
-## References
-
-- [ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md)
-- [research 012, *migrating a node that is in use*](../01-RESEARCH/012-the-minimum-viable-node/migrating-a-node-in-use.md)
diff --git a/02-DECISIONS/0102-the-mesh-writes-into-a-shared-file-never-over-it.md b/02-DECISIONS/0102-the-mesh-writes-into-a-shared-file-never-over-it.md
deleted file mode 100644
index 9be072ab..00000000
--- a/02-DECISIONS/0102-the-mesh-writes-into-a-shared-file-never-over-it.md
+++ /dev/null
@@ -1,102 +0,0 @@
----
-topic: the mesh
-status: accepted
-date: 2026-09-22
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md
----
-
-# 102. The mesh writes into a shared file, never over it
-
-## Context
-
-[Issue 084](../04-ISSUES/084-taking-networking-on-an-adopted-node-restarts-every-container/00-report.md)
-found that the networking module declares the container runtime's configuration file to state
-one fact in it: the mesh's registry is trusted over the private network. Reading the code
-closer showed it worse than reported. The controller merges an operator's settings into the
-module's own content, but the host writes the result **whole**. Whatever the machine had in that
-file is replaced, including where the runtime keeps its data. On a machine in use, that is every
-image and container gone from the runtime's view at its next start. The runtime's service is then
-restarted, which stops every container on the machine.
-
-Measured on a lab machine: the runtime takes a new list of trusted registries on a reload, with
-no restart, and a running container with no restart policy keeps running through it. The
-runtime's log says it reloaded its configuration, and the registry reads as trusted afterwards.
-
-Under [ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md), the file is found
-on an adopted node and held until networking is taken. Holding it is safe, but it means an
-adopted machine cannot pull the mesh's images until then, and taking networking would restart
-the runtime.
-
-## Considered Options
-
-1. **Keep writing the whole file; take networking as a cutover of its own.** Rejected: it
- still replaces the machine's settings, and the cutover stops everything on the machine.
-2. **A drop-in the runtime reads beside its main file.** Rejected: the runtime has no such
- directory for its daemon settings.
-3. **Write into the file: set the mesh's keys, keep the rest; reload, don't restart.** Adopted.
-
-## Decision
-
-**A file the mesh shares with software it did not install is written into, never over.** A
-file resource may say it is written *into* a structured file. The host then reads what is there,
-sets only the keys the mesh declares, keeps every other key as it found it, and records what each
-of its keys held before. **A list is added to, never replaced**: where the machine already has a
-list under a key the mesh declares, the mesh's members are added to it and the host records
-exactly which members it added — setting the key would replace the operator's own list, the harm
-this record exists to prevent. Undeclared later, each key goes back to what it held and each
-added member is taken out again, and a file the mesh created is removed only if nothing but its
-own keys is left. A file written into replaces nothing, so on an adopted node it is never held:
-it is written whether or not its module has been taken.
-
-**Whatever the host writes over without a record of it, it keeps first.** On any node, adopted
-or converged, before the host writes a file over one it has no record of making, it keeps the
-original once and says where; if it cannot keep it, it does not write. A file the mesh takes over
-is then never lost, whatever put it there.
-
-**A service that re-reads its configuration on a reload is reloaded, not restarted.** A service
-resource may name what it must be *reloaded* on, beside what it must be restarted on. The
-container runtime is reloaded for the registry's trust.
-
-**The networking module writes the runtime's trust into its file and reloads it.** An adopted
-node therefore trusts the mesh's registry as soon as it is on the private network, and taking
-networking no longer touches the runtime. The hosts file networking writes is still written whole
-and stays held until networking is taken; a converge preview names it among the files it replaces.
-
-> **The mechanism changed — 2026-10-05, by [ADR 0222](0222-a-module-is-told-where-a-mesh-seats-holder-is-reached-and-the-controller-writes-no-file-a-seats-holder-owns.md).**
-> Writing into a shared file, adding to a list and reloading rather than restarting all stand. What
-> moved is the writer: the networking module no longer writes the runtime's trust or declares its
-> service. The container runtime's own module writes it into its own file and reloads its own
-> service ([issue 190](../04-ISSUES/190-the-runtimes-configuration-is-written-by-modules-that-are-not-the-runtime/00-report.md)).
-> The controller test named under *How it is checked* below, which held the networking module to
-> declaring the runtime's file, is replaced by one holding the networking module to declaring neither,
-> and by one holding the runtime's module to the trust.
-
-## Consequences
-
-- The runtime's file on a machine in use keeps its data directory, its logging settings and
- everything else the predecessor set.
-- A host that does not know *into* or *reload-on* refuses a declaration carrying them, so hosts
- are upgraded before the controller that emits them — the same order ADR 0100 needs.
-- One shape more for every host: a file written into a structured document. Only JSON is spoken;
- another format is refused until written.
-- The hosts file remains a whole file. Writing a marked block into it is the same idea for a text
- file and is not decided here.
-
-## How it is checked
-
-Unit tests hold the host to setting only the declared keys and keeping the rest, restoring each
-key and removing only a file it created when the resource is undeclared, adding to a list and
-removing only the members it added, keeping the original of a file it writes over without a
-record, refusing a file that is not a JSON object rather than overwriting it, never holding a file written into on an adopted
-node, and reloading rather than restarting a service whose reload-on resource changed. A test in
-the controller holds the networking module to declaring the runtime's file written into and the
-runtime reloaded. The adoption lab bed gives the machine a runtime file with a setting of its own
-and asserts it survives adoption with the registry trusted added, and that a container without a
-restart policy is still running afterwards.
-
-## References
-
-- [Issue 084](../04-ISSUES/084-taking-networking-on-an-adopted-node-restarts-every-container/00-report.md)
-- [ADR 0082](0082-the-registry-is-reached-by-name-and-trusted-by-the-overlay.md), [ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md)
diff --git a/02-DECISIONS/0103-what-an-adopted-node-holds-and-what-its-guard-refuses.md b/02-DECISIONS/0103-what-an-adopted-node-holds-and-what-its-guard-refuses.md
deleted file mode 100644
index 79557379..00000000
--- a/02-DECISIONS/0103-what-an-adopted-node-holds-and-what-its-guard-refuses.md
+++ /dev/null
@@ -1,112 +0,0 @@
----
-topic: the mesh
-status: accepted
-date: 2026-09-22
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md
----
-
-# 103. What an adopted node holds, and what its guard refuses
-
-## Context
-
-[ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md) holds a found file or
-container until its module is taken. It also has the mesh guard the store's port and the
-broker's management port with a table that only refuses. Two independent reviews of the build
-found both rules drawn too narrowly and, for the guard, in the wrong place:
-
-1. **Other kinds of resource reach what was found.** An untaken module's directory is re-owned
- and re-moded if a predecessor's directory is already at its path, and a database refuses to
- start on a data directory whose mode changed. A service of the same name as a predecessor's
- unit is started, stopped or re-enabled. An action run *in* a held container runs inside the
- predecessor's service. A container that is not found under its own name is created, and can
- mount the predecessor's data beside the predecessor's own container.
-2. **The guard's two ports are not the only ones a found firewall misses.** A published
- container port is forwarded, not received, and a firewall that filters only incoming traffic
- never sees it (ADR 0100's own Context). The broker's plaintext port is published on every
- interface and admitted by the filter from the private network only. So on an adopted node it
- is reachable from anywhere. The same holds for any published port a module restricts to the
- private network.
-3. **The guard refuses by port alone.** On a machine that routes for others, such as a
- predecessor's private-network hub, a packet for another machine's database port is refused as
- well. And a port the guard refuses for a module that is assigned but not taken may still be
- the predecessor's own, serving the predecessor's other machines.
-
-## Decision
-
-**Found covers every kind that can reach what the machine already has.** On an adopted node,
-for a module not yet taken:
-
-- a **directory** present with no record is held: its mode and owner are left, and nothing in it
- is touched;
-- a **service** with no record is held when an administrator installed its unit — the service
- manager reads the unit from outside the packages' own directory — or when the machine uses it,
- running or started at boot. Its state and whether it starts at boot are then left. A reload
- named by the module still happens, since a reload stops nothing
- ([ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md)). A unit a package merely
- ships that nothing runs and nothing enables is not found: the private network's own tunnel unit
- is one, and holding it kept the private network from ever coming up;
-- an **archive** whose target is present with no record is not unpacked or re-owned; a
- **process** whose unit file is present with no record is not written over or restarted; a
- **user** that exists with no record keeps its shell and groups;
-- a **container** not found by its name is still held if it would mount a path or a volume that
- is present with no record, because it would share the predecessor's data;
-- an **action** or one-off step run *in* a held container is held until the module is taken.
-
-What is held is reported as held and is never removed, as ADR 0100 decides for files and
-containers.
-
-**The guard refuses what the found firewall may miss, for taken modules only.** Its ports are
-derived from each *taken* module: every machine port it publishes that the filter would admit
-from the private network only, and the ports its manifest names under `guards` — which is how
-the store's port and the broker's management port are included. Only TCP is guarded. A port of a module that is assigned but not taken is not guarded: it may still
-be the predecessor's. The guard matches only packets addressed to this machine. Traffic the
-machine routes for others is never its business.
-
-**An opening already answered by a found rule is not added.** If the found firewall already
-admits what an opening says, the opening is reported as satisfied by the found rule, and the
-mesh adds nothing and later removes nothing. The found firewall treats two rules differing only
-in their action, log setting or comment as one rule, so adding the mesh's would take over the
-operator's. Where the found rule is such a twin but is not a plain allow — a deny, a limit, a
-logged allow — the opening is refused, naming that rule, and nothing is added.
-
-**A machine raised adopted stays adopted if genesis is run again.** The installer reads the
-node's mode from what the machine records, not only from the operator's flag. A run without
-the flag on an adopted machine is refused.
-
-**What of ADR 0100 this replaces.** ADR 0100's guard refused two ports, the foundation's own,
-checked free at genesis, and could therefore close nothing the machine served. That guard is
-replaced by the one above: a guarded port of a taken module may be one the predecessor served
-more widely, and taking the module narrows it to the private network. Everything else in ADR 0100
-stands, and its rules for files and containers now cover the other kinds listed here.
-
-## Consequences
-
-- Taking a module can narrow a port the predecessor served to anyone: the guard then refuses it
- from outside the private network, as the module declares. Nothing says so yet
- ([issue 086](../04-ISSUES/086-taking-a-module-narrows-a-port-without-saying-so/00-report.md)).
-- An untaken module on an adopted node can come up only beside what was found, never on top of
- it: whatever would share the predecessor's data waits for the cutover.
-- The guard grows with the modules taken, and a module's published private-network port is
- protected on an adopted node the way the derived filter protects it on a converged one.
-- Guarding only taken modules means a foundation port on a node joining adopted is guarded once
- its module is taken, not when it is assigned. Genesis takes the foundation's modules, so the
- control-node's store is guarded from the first push.
-
-## How it is checked
-
-Unit tests hold the host to holding a found directory, a found service, a found archive, process
-and user, and a container that would mount found data; to not holding a unit nothing runs; to
-deferring an action run in a held container; to adding no opening a found rule answers, and to
-refusing one a conflicting found rule would absorb. They hold the controller to deriving the guard from taken modules'
-private-network published ports, and the guard's text to matching only this machine's
-addresses. The installer's tests hold a re-run without the flag on an adopted machine to a
-refusal. The adoption lab bed asserts that the broker's plaintext port is unreachable from
-outside the private network even with the found firewall admitting it, and that an operator's
-own rule for a port the mesh opens survives the opening being removed.
-
-## References
-
-- [ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md),
- [ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md)
diff --git a/02-DECISIONS/0104-a-provision-may-be-answered-by-an-adapter-to-the-predecessor.md b/02-DECISIONS/0104-a-provision-may-be-answered-by-an-adapter-to-the-predecessor.md
deleted file mode 100644
index 35675c9b..00000000
--- a/02-DECISIONS/0104-a-provision-may-be-answered-by-an-adapter-to-the-predecessor.md
+++ /dev/null
@@ -1,93 +0,0 @@
----
-topic: the mesh
-status: accepted
-date: 2026-09-22
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md
----
-
-# 104. A provision may be answered by an adapter to the predecessor
-
-## Context
-
-[Issue 093](../04-ISSUES/093-the-successor-proxy-cannot-serve-what-the-predecessor-still-serves/00-report.md),
-found on the first adopted node hours after it was raised. Every module reachable by name requires
-a **route**. The mesh has one provider of it, its own proxy, which binds the two public ports on
-the machine's own network. The predecessor's proxy holds those ports and serves every public name
-there. So:
-
-- a web module cannot be taken until the mesh's proxy runs;
-- the mesh's proxy cannot run until the predecessor's stops;
-- and when it stops, every name the predecessor served goes dark, because the mesh's proxy serves
- only what mesh modules have contributed.
-
-Adoption ([ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md)) keeps a *file* as it
-was and a *container* as it was. It has nothing for a service whose successor is **different
-software**: nothing shares a name to hold, so there is only replacement, of every name at once.
-
-The certificates decide the rest. The predecessor's proxy holds them and renews them. A successor
-taking over first would have to obtain a certificate for every public name in the same window in
-which it took the ports — with nothing else migrated yet, and the only way back being to start the
-predecessor again.
-
-## Considered Options
-
-1. **The successor proxy serves routes it did not derive**, given as the operator's configuration,
- each disappearing as the module owning that name contributes its own. Rejected for the
- migration: it puts the riskiest step first, and it puts configuration the mesh does not own
- into the one component whose design is that the mesh derives its configuration.
-2. **Take the proxy first and accept the window.** Rejected: every public name at once, before
- anything else has moved, with certificates to obtain in the same window.
-3. **Make the requirement optional**, so a module can be taken while nothing provides route.
- Rejected: it is not optional. A module that says it must be reachable and is not is a module
- reporting success while serving nobody.
-4. **An adapter module provides the provision by writing into the predecessor's service.** Adopted.
-
-## Decision
-
-**A provision whose only provider owns a scarce machine-wide resource may be answered, during a
-migration, by an adapter module that writes into the predecessor's own configuration.**
-
-For the route: a module that **provides `route`**, receives every contribution as the proxy does,
-and writes each one as a route file where the predecessor's proxy reads its configuration —
-pointing at the machine port the contributing module now publishes. The predecessor's proxy keeps
-serving every name it already serves, keeps its certificates and keeps renewing them; a name whose
-module has migrated is served by the same proxy, pointing at the mesh's container instead of the
-predecessor's.
-
-**It is migration scaffolding and says so.** It is assigned only on an adopted node, it writes only
-files it can name as its own, and it is removed when the predecessor's proxy retires — at which
-point the mesh's own proxy takes the ports and already knows every route, because by then every
-name belongs to a module that contributes it.
-
-**This does not make the mesh the predecessor's manager.** The adapter writes route files and
-nothing else, and only because the predecessor's control has been stopped by the operator, which
-ADR 0100 requires before a node is adopted.
-
-## Consequences
-
-- The migration keeps its shape: one service at a time, each reversible on its own. The proxy is
- the **last** cutover again, not the first.
-- There is no certificate event until the end, and at the end there is one, for a proxy that
- already has every route from the mesh.
-- The mesh writes into a directory the predecessor owns. That is safe only while the predecessor's
- control is stopped, and it is why the adapter is refused on a converged node.
-- The adapter is throwaway code with a stated end. Something has to delete it; that something is
- the operator, and the flip is the moment.
-- A second provision with this shape — a resolver on 53, a mail relay on 25 — now has a pattern to
- follow rather than a new argument to have.
-
-## How it is checked
-
-Unit tests hold the adapter to writing one route file per contribution, naming each file as its
-own, removing a file when its contribution goes, and leaving every file it did not write. The
-controller is held to resolving `route` from it exactly as from the proxy. The adoption lab bed
-gains a step: with the predecessor's proxy serving a name, a module taken behind the adapter is
-reachable under that name, and the predecessor's own names keep answering. Assigning the adapter
-to a converged node is refused.
-
-## References
-
-- [Issue 093](../04-ISSUES/093-the-successor-proxy-cannot-serve-what-the-predecessor-still-serves/00-report.md)
-- [ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md), [ADR 0007](0007-connectivity.md)
diff --git a/02-DECISIONS/0105-the-mesh-adopts-the-predecessors-tunnel-in-place.md b/02-DECISIONS/0105-the-mesh-adopts-the-predecessors-tunnel-in-place.md
deleted file mode 100644
index 0fb7080c..00000000
--- a/02-DECISIONS/0105-the-mesh-adopts-the-predecessors-tunnel-in-place.md
+++ /dev/null
@@ -1,148 +0,0 @@
----
-topic: the mesh
-status: accepted
-date: 2026-09-23
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md
-supersedes-in-part:
- - 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md
----
-
-# 105. The mesh adopts the predecessor's tunnel in place
-
-## Context
-
-The control-node is adopted and its store and broker have been moved onto the ports the
-predecessor served them on, so the predecessor's other machines keep reaching them. They reach
-them **over the predecessor's tunnel**: a WireGuard interface on the control-node with three
-peers, an address range, and a port the hosting provider already lets through. The mesh's own
-private network runs beside it on a second interface, a second range and a second port — one the
-provider does not let through, so no other machine can join the mesh
-([research 012](../01-RESEARCH/012-the-minimum-viable-node/00-overview.md), the runbook's
-measurement of the upstream filter).
-
-[ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md) says the mesh's range *must not
-overlap a tunnel the predecessor still runs*. That was written for coexistence. It leaves the
-migration with two tunnels for as long as any predecessor machine exists, and the second one
-unreachable.
-
-The mesh already adopts in place where the thing found is the thing it would have raised: the
-store and the broker were taken over as modules, keyed on the container that was running
-([ADR 0078](0078-the-store-and-broker-are-modules.md)). A WireGuard interface is the same shape: a
-private key, a listening port, a list of peers by public key, an address. The mesh's is not
-different in kind from the predecessor's; it is a second one.
-
-The operator's instruction: take over the tunnel interface, same range — everything stays the same.
-
-## Considered Options
-
-1. **Two tunnels until the last predecessor machine is gone.** Rejected: the mesh's stays
- unreachable from outside, so no machine can join, so the last predecessor machine is never gone.
-2. **Move the mesh's tunnel onto the predecessor's port with the mesh's own key and range.** The
- peers' packets arrive and are dropped — WireGuard authenticates by key, and the mesh's key is not
- the one they know. Every other machine loses its tunnel until it enrols, and it enrols over a bus
- it reaches through that tunnel. Rejected.
-3. **Adopt the predecessor's tunnel in place: its private key, its peers, its range, its port.**
- Adopted.
-
-## Decision
-
-**On an adopted node that is the hub, the private network takes over the tunnel it finds.** The
-mesh's interface is raised with the found interface's **private key**, on its **port**, with its
-**address and range**, and every **peer** the found interface had — public key, allowed address —
-carried into the mesh's peer list as a peer not yet enrolled. The found interface is stopped, never
-flushed; its configuration stays on disk, kept like any held file.
-
-**Nothing a peer knows changes.** A predecessor machine keeps the same server key, the same
-endpoint, the same address and the same route; it cannot tell the tunnel changed hands. When that
-machine enrols, it keeps its address: the mesh assigns an enrolling node the address the tunnel
-already had for its key, and only a node with no such address is given a fresh one from the range.
-
-**The mesh's own addresses are the range's.** The controller composes every node's private address
-from the tunnel it holds, so adopting the predecessor's range moves the mesh's addresses with it —
-the hub's, and every binding, hosts-file entry and endpoint derived from it. Those are readers of
-the setting; they follow it, per ADR 0100's rule for ports. A reader that does not follow is
-[issue 102](../04-ISSUES/102-an-address-recorded-at-genesis-or-build-does-not-follow-the-nodes-ports/00-report.md).
-
-**This narrows ADR 0100.** Its rule that the range must not overlap a tunnel the predecessor still
-runs applies to a node that is *not* adopting the tunnel: where the found tunnel is left running
-beside the mesh's, the ranges must differ. Where it is adopted, there is one tunnel and one range.
-
-**The guard's question answers itself.** [ADR 0103](0103-what-an-adopted-node-holds-and-what-its-guard-refuses.md)
-admits the mesh's own ports only from the private network's interface. With one tunnel, the
-predecessor's peers arrive on it, and nothing needs to be admitted from an interface the mesh does
-not own.
-
-## Consequences
-
-- **The order of a migration changes.** The hub's tunnel can be taken as soon as the node is
- adopted, before any service — and should be, because it is what lets other machines join. The
- runbook's step order is amended.
-- **The hub takes the provider's open port for free.** The port the predecessor's tunnel used is
- by definition one the provider passes.
-- **A peer's identity precedes its enrolment.** The mesh holds public keys and addresses for
- machines it has no record of. They are peers of the tunnel, not nodes of the mesh, until they
- enrol; the registry must be able to say both.
-- **What got harder:** the private key of the found interface is read from the machine and becomes
- the mesh's — the one case where the mesh takes a credential it did not mint. It is sealed like
- any own secret from then on, and the found configuration file is kept, not copied further.
-- Two documents currently say the opposite: ADR 0100's non-overlap rule (narrowed above) and the
- runbook's port plan, which is amended with this record.
-
-## How it is checked
-
-A lab bed prepares a hub the way the predecessor leaves one: a WireGuard interface with a key, a
-port, a range and two peers, each peer a second machine that reaches a service on the hub through
-the tunnel. Then:
-
-- **Adopted, the tunnel changes hands and the peers notice nothing**: the found interface is down
- and its file is on disk; the mesh's interface is up with the found key, port and address; each
- peer's service call succeeds before, during and after, with no reconfiguration on the peer.
-- **A peer enrols and keeps its address**: the machine joins the mesh over the tunnel it already
- has, and its node address is the one the tunnel held for it.
-- **A new machine gets a fresh address from the same range**, and reaches both the hub and the
- enrolled peer.
-- **Nothing derived from the address is stale**: every binding, hosts entry and endpoint the
- controller composes says the adopted range, before and after a push.
-
-Unit tests hold the controller to reading the hub's address and range from the adopted tunnel,
-assigning an enrolling node the address its key already had, and refusing to hand out an address
-the tunnel already holds; and the host to raising the mesh's interface with the found key and
-peers and stopping the found interface without flushing it.
-
-## What review settled that this record did not
-
-*Added 2026-09-24, from the review of the implementation. A decision record is not edited to change
-its meaning; this says what was decided under it.*
-
-- **A spoke's view of its hub is not a peer the mesh carries.** A predecessor gives a spoke the
- whole subnet through the hub, so the spoke's found tunnel names one peer routed a range rather
- than an address. Only the hub's peers are ever carried; a spoke presenting its own is skipped,
- not refused — a machine enrols with what it found, and what it found is its route home.
-- **The range and the carried peers outlive the flip.** They follow from the hub having taken the
- tunnel over — its key being the tunnel's — and not from the node being adopted. A converged hub
- keeps the range it adopted and the addresses it is holding, and `AssignAddress` keeps excluding
- them.
-- **Converging the hub is not refused while a carried peer has not enrolled.** Proposed in review
- and rejected on the record's own terms: *a node converges when its migration is done*, and the
- other machines' migrations are not this node's. With the range and the peers surviving the flip
- there is nothing left for the refusal to protect, and it would have made one machine's converge
- wait on every other machine.
-- **A takeover whose placement disagrees with the tunnel is refused before it is composed** — an
- address or a port that is not the tunnel's would stop the found interface and raise the mesh's
- somewhere the peers are not, while reporting success.
-- **The host says three things, not two**: the found interface still up, the mesh's up in its place,
- or — the state worth naming — the found one down and the mesh's not up, which is the only one
- where the peers reach nothing.
-- **A hub that enrolled before this existed keeps its identity.** Re-enrolling would have remade
- every credential in the mesh, because the hub provides the store and the broker. Instead the
- machine takes the tunnel's key as its overlay key and says so in a message signed with the
- identity it already has, so a forged report cannot move a node's key.
-
-## References
-
-- [ADR 0078](0078-the-store-and-broker-are-modules.md), [ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md),
- [ADR 0103](0103-what-an-adopted-node-holds-and-what-its-guard-refuses.md)
-- [issue 102](../04-ISSUES/102-an-address-recorded-at-genesis-or-build-does-not-follow-the-nodes-ports/00-report.md)
-- [research 012 — the minimum viable node](../01-RESEARCH/012-the-minimum-viable-node/00-overview.md)
diff --git a/02-DECISIONS/0106-the-bus-is-nats.md b/02-DECISIONS/0106-the-bus-is-nats.md
deleted file mode 100644
index 47decabf..00000000
--- a/02-DECISIONS/0106-the-bus-is-nats.md
+++ /dev/null
@@ -1,107 +0,0 @@
----
-topic: the mesh
-status: accepted
-date: 2026-09-23
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0002-nodes-communicate-over-a-broker.md
----
-
-# 106. The bus is NATS
-
-## Context
-
-The mesh's bus is an AMQP broker. [Research 014](../01-RESEARCH/014-the-bus-on-nats/00-overview.md)
-measured what that costs and what it would take to change: AMQP is spoken in three places of the
-mesh's own code — the controller's link, the host's link, the tool runtime's client — and in none of
-the sdk or the modules, because [ADR 0039](0039-what-the-sdk-holds-and-refuses.md) kept the client
-out of the sdk for exactly this. The predecessor's world is AMQP and is being retired module by
-module; it cannot move and does not need to.
-
-The operator's direction: NATS, native — the mesh built on it, not bridged to it — and the tools a
-person reaches from a workstation designed on it rather than built quickly on what exists.
-
-## Considered Options
-
-1. **Stay on AMQP.** Rejected by the operator.
-2. **NATS behind a bridge**, the mesh unchanged. Rejected: two buses, every guarantee crossing a
- seam, and the reason for changing — one native, simple, subject-addressed bus with request/reply
- and accounts built in — lost at the seam.
-3. **NATS native, built in the lab in parallel with the migration, cut over in one rehearsed
- rollout after the migration's core is done.** Adopted.
-
-## Decision
-
-**The mesh's bus is NATS.** Every link the mesh has — control, node queues, builds, enrolment,
-reports, events, tool invocation — is carried on NATS subjects; durability, catch-up and
-hold-unacknowledged-and-retry ([ADR 0083](0083-one-push-leaves-the-mesh-consistent.md)) are
-JetStream's; a module's account is a NATS account with per-subject permissions derived from its
-`emits` and `consumes` ([ADR 0043](0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md)),
-written as configuration the host declares and the server reloads — never through a management
-API. The broker module changes; the seat `mesh-broker` does not
-([ADR 0079](0079-the-foundation-seats-are-named-after-their-servers.md)).
-
-**The sdk's broker contract does not change.** Modules are written against `request`, `handle`,
-`publish`, `subscribe`; the runtime implements them on NATS; no module speaks a protocol.
-
-**The AMQP broker the mesh adopted becomes the predecessor's compatibility broker**, kept as a
-module with one purpose — the predecessor's clients — and retired with the last of them.
-
-**It is built beside the migration and cut over after its core.** The bus is not changed under a
-half-migrated node. A lab bed proves the whole path — enrolment, the store window, upgrades, tool
-invocation — on NATS before any node's bus moves, and the move is one rollout: controller, every
-host, every runtime together.
-
-**What a person reaches the mesh's tools with is designed on NATS**, as part of the same design: a
-person's account, scoped like a module's, and a client that speaks the bus directly.
-
-## Consequences
-
-- The architecture — subjects, streams, accounts, the enrolment handshake, the person's client — is
- written as a to-be design before code, and reviewed.
-- Eight records are touched: 0002 and 0033 (the substrate names a broker — now NATS), 0041 and
- 0042 (events keep their shape; the exchange becomes a subject prefix), 0043 (accounts as
- configuration), 0078 (the broker module is `nats`), 0083 (JetStream carries the guarantee), 0039
- (unchanged, and the reason this is possible).
-- The lab beds that prove the bus are re-run on NATS; none is skipped.
-- Until the cutover, nothing changes on any node.
-
-## How it is checked
-
-A lab bed raises a mesh on NATS from genesis and proves: a node enrols over TLS with a claimed
-token; a push composes, is held while the store restarts and applies after; an upgrade rolls out;
-a module's tools are invoked from another node and from a person's client; an event dead-letters
-after its deliveries are exhausted; a module's account cannot publish outside its `emits` nor
-subscribe outside its `consumes`. Then the cutover bed: a mesh on AMQP with the predecessor's
-compatibility broker beside it moves its bus in one rollout with every node reporting afterwards.
-
-## Progressive insight
-
-> **Progressive insight — 2026-09-26.** *The compatibility broker was not single-purpose when this
-> was written.* This record says the adopted AMQP broker is "kept as a module with one purpose —
-> the predecessor's clients". Two modules of the new mesh also depended on it, through a `requires:
-> ["amqp"]` grant its provisioner answered with a private vhost — `amqp-ping` and
-> `amqp-email-forwarder`. On the retirement condition below, both would have been left requiring
-> something no provider answers.
-> [ADR 0125](0125-the-bus-is-the-only-broker.md) resolves it by moving them onto the bus and
-> retiring the interface, which makes this record's sentence true rather than merely intended. The
-> decision — the bus is NATS, the AMQP broker becomes the predecessor's compatibility broker and
-> retires with the last of them — is unchanged.
-
-> **Progressive insight — 2026-09-26, correcting the one above.** *The broker is not a
-> compatibility module at all, and the sentence does not become true.* The insight above said
-> [ADR 0125](0125-the-bus-is-the-only-broker.md) would make "one purpose — the predecessor's
-> clients" true by moving the mesh's own modules off it.
-> [ADR 0127](0127-amqp-is-a-provision-not-the-bus.md) supersedes that: nothing moves off, because
-> a module may legitimately need an AMQP broker as a backing service the way it needs a database.
-> The broker becomes **an ordinary provider module** — no seat, not foundation, not raised at
-> genesis, and with no retirement condition, because the day its last client disappears is not a
-> day anything is waiting for. What this record decided — the mesh's bus is NATS — is untouched
-> by both; what was wrong was the sentence describing what happens to the old server, twice.
-
-## References
-
-- [research 014](../01-RESEARCH/014-the-bus-on-nats/00-overview.md)
-- [ADR 0002](0002-nodes-communicate-over-a-broker.md), [0033](0033-the-substrate-is-a-store-and-a-broker.md),
- [0039](0039-what-the-sdk-holds-and-refuses.md), [0043](0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md),
- [0078](0078-the-store-and-broker-are-modules.md), [0083](0083-one-push-leaves-the-mesh-consistent.md)
diff --git a/02-DECISIONS/0107-persistent-data-is-a-directory-bind-never-a-named-volume.md b/02-DECISIONS/0107-persistent-data-is-a-directory-bind-never-a-named-volume.md
deleted file mode 100644
index 20544547..00000000
--- a/02-DECISIONS/0107-persistent-data-is-a-directory-bind-never-a-named-volume.md
+++ /dev/null
@@ -1,71 +0,0 @@
----
-topic: building it
-status: accepted
-date: 2026-09-24
-deciders: jochen
-reconstructed: false
----
-
-# 107. Persistent data is a directory bind, never a named volume
-
-## Context
-
-Measured 2026-09-24, mid-migration: four modules mount a named Docker volume for real state —
-`mesh-store` (every database the mesh holds), `mesh-broker` (its data and TLS material),
-`mesh-registry` (every image), and `searxng`'s cache. Every other module in the catalogue —
-more than forty — mounts a host directory, `/var/lib//...`.
-
-The predecessor did not make this choice. HAL's own `postgres` bound `./db-data`, and its
-`lavinmq` bound `./data` — directories, both. The mesh's adoption of them
-([`a63ef3d`](https://git.novox.be/novox/mesh-catalog/commit/a63ef3d), "the postgres module
-adopts mesh-store instead of raising its own", 2026-09-16) introduced the named volume; three
-other modules followed the same shape since. Full account of what was found and fixed:
-[issue 115](../04-ISSUES/115-a-named-docker-volume-is-invisible-and-one-flag-from-gone/00-report.md).
-
-## The two guarantees are not the same guarantee
-
-A named volume and a host directory both survive **ordinary** container recreation — a rebuild,
-a `take`, the `docker rm -f` and push this migration already uses routinely. Neither loses data
-to that. That was never the question.
-
-What they do not both survive:
-
-- **`docker rm -fv`, `docker volume rm`, `docker system prune --volumes`** all target a named
- volume specifically. The first is one character from the command this migration's own rules
- already call for after every address change. A host directory has no equivalent command that
- destroys it by accident — removing it is always a deliberate `rm -rf` on a path someone typed.
-- **Visibility.** Every tool this migration has used all night to find and verify data —
- `ls`, `find`, `grep`, a backup job — reaches a host directory for free. A named volume requires
- knowing to ask Docker (`docker volume inspect`) before its bytes, at
- `/var/lib/docker/volumes//_data`, are reachable at all.
-
-## Decision
-
-**A container mount holding data that must survive is a host directory bind. A named volume is
-permitted only for data that is disposable if lost** — a cache, a scratch space, something the
-module rebuilds on next start without consequence. `searxng`'s `valkey` cache is close to this
-line and was converted anyway, for consistency and because it costs nothing to.
-
-Ownership is the one thing a host directory does not get for free that a named volume does:
-Docker initialises a fresh named volume's ownership to what the container's first process needs;
-a host directory is whatever created it. A directory made for this purpose must be given the
-image's expected UID before the container using it starts — read from the running instance being
-replaced when one exists, rather than guessed.
-
-## Consequences
-
-- The four modules were converted: `distribution`, `lavinmq`, `postgres`, `searxng`. Data copied
- and verified byte-for-byte before each manifest changed; `mesh-store` stopped cleanly first, so
- its copy is crash-consistent rather than a live read of a running postgres.
-- **The ownership gap above was not theoretical — it is what happened.** The new directories
- were created by the operator's tooling running as root; `mesh-store` crash-looped on
- `mkdir: ... Permission denied` until its directory's ownership was set to match what the
- original volume already had. Worth a check at `module add` time — nothing catches this today
- beyond the container failing to start.
-- Old named volumes were not deleted. They remain the rollback path until confidence in the new
- mounts is established over time, not one clean start.
-
-## References
-
-- [Issue 115](../04-ISSUES/115-a-named-docker-volume-is-invisible-and-one-flag-from-gone/00-report.md)
-- `mesh-catalog` PR #54
diff --git a/02-DECISIONS/0108-a-route-carries-the-policy-applied-to-a-request.md b/02-DECISIONS/0108-a-route-carries-the-policy-applied-to-a-request.md
deleted file mode 100644
index d7e46d3e..00000000
--- a/02-DECISIONS/0108-a-route-carries-the-policy-applied-to-a-request.md
+++ /dev/null
@@ -1,102 +0,0 @@
----
-topic: the tiers
-status: accepted
-date: 2026-09-25
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0007-connectivity.md
----
-
-# 108. A route carries the policy applied to a request, and names a secret rather than holding one
-
-## Context
-
-[ADR 0007](0007-connectivity.md) settled that **a route is a grant**: a module contributes the name
-it wants and the port it listens on, and the proxy hands back the public name. That governs the
-**grant**. It says nothing about what a request arriving at the name is permitted to do, and the
-mesh's proxy currently permits everything: its request path is a host lookup and a forward, with no
-authentication, no source check, no redirect handling and no middleware anywhere in it.
-
-The standing requirement is that the mesh does **at minimum** what the system it replaces already
-does. Measured against the predecessor's live configuration and its module catalogue
-([issue 116](../04-ISSUES/116-route-proxy-has-no-auth-or-ip-restriction/00-report.md)), four
-capabilities are relied on and absent:
-
-| Capability | Dependents, counted |
-|---|---|
-| Authentication | **three** modules, each gating a credential-less admin surface — a key-value browser UI, a **database web UI**, and the replaced ingress's own dashboard |
-| Refusal scoped to a path | **one**, and it is a live **incident mitigation** closing a write primitive that was abused |
-| Path-scoped routing with priority | **two** — the refusal above, and a certificate-challenge path on a host that otherwise routes elsewhere |
-| Redirect | **two** live routes canonicalising a `www` name onto its apex |
-
-Counting needed two sources and neither alone is complete: the catalogue cannot see what was
-hand-written on a node, and a node's configuration directory cannot see what modules declare as
-container labels. An earlier count read one source and undercounted authentication by two.
-
-**The third row is a prerequisite, not a sibling.** The proxy's table maps a host to exactly one
-target, so a host cannot be routed two ways. Adding authentication and a source filter would not
-make the refusal rule expressible.
-
-## Considered Options
-
-1. **Leave policy out of the mesh; keep the affected routes on the adopted ingress.** *Rejected.*
- The mesh would run two reverse proxies indefinitely with no principled division between them, and
- one of the routes held back is a live mitigation — leaving it on a component being decommissioned
- means its removal date is whenever somebody forgets.
-2. **A separate, proxy-side settings layer keyed by route name.** *Rejected.* It keeps the grant
- literally clean, but answering *"what protects this route"* then requires reading two files that
- nothing keeps in step. A route's protection is part of what a route is.
-3. **The grant carries a reference; the detail lives in a second layer.** *Rejected.* Both costs of
- option 2, plus a naming indirection to maintain.
-4. **Carry the password hash in the declaration.** *Rejected.* It would be the first credential
- value in a declaration, and a precedent is easier to set than to withdraw. A hash is not a
- plaintext password, but it is sufficient to pass the gate it protects.
-5. **An open middleware surface the proxy applies.** *Rejected.* It recreates the thing being
- replaced, makes the proxy's behaviour unbounded, and an open surface is far harder to narrow later
- than a closed one is to widen.
-
-## Decision
-
-**A route contribution carries the policy applied to requests arriving at its name**, alongside the
-name, the port and the location it already carries.
-
-**The set is closed, and it is these four:** authentication; refusal scoped to a path; path-scoped
-routing with priority; redirect. A fifth is an amendment to this record, deliberately — each
-addition should be earned by a dependent that exists.
-
-**Where policy needs a credential, the declaration names a secret the mesh mints and holds. It never
-carries the value.** This keeps the existing secret machinery as the only thing that holds
-credentials, and keeps hashes out of anything regenerated, synced or committed.
-
-**Consequently the routing table is keyed by host and path, with priority** — not by host alone.
-This follows from the decision rather than being a separate one: two of the four capabilities need a
-single host routed more than one way.
-
-## Consequences
-
-**What this makes possible.** The affected routes can leave the adopted ingress, and "at minimum"
-becomes a satisfiable claim rather than a standing exception. The incident mitigation gets a durable
-home in the mesh, which its own note already asked for.
-
-**What got harder.** The contribution shape grows, and every provider of `route` must understand
-more of it. The table is no longer a flat map, and priority introduces ordering that has to be
-deterministic rather than incidental — equal priorities must resolve the same way every time or the
-proxy becomes non-reproducible. A closed set means a new need is a decision, not a patch.
-
-**What does not change.** The proxy remains a reference implementation: the contract is the file the
-mesh writes, not the program that reads it. Another proxy may implement the same file.
-
-**How this is checked.** A rule states how it is verified, so this one does. A lab bed must show,
-against a mesh that declared them: a route with authentication refusing an unauthenticated request
-and admitting an authenticated one; a path-scoped refusal shadowing an ordinary route on the same
-host while that ordinary route still serves every other path; a redirect answering with the
-redirect; and — the negative case, which is the one that rots quietly — **a declaration carrying a
-credential value rather than a reference being refused**, so option 4 cannot return by accident.
-
-## References
-
-- [Issue 116](../04-ISSUES/116-route-proxy-has-no-auth-or-ip-restriction/00-report.md) — the count,
- the evidence, and the structural finding about the table.
-- [ADR 0007](0007-connectivity.md) — a route is a grant. This record extends it to the request.
-- [ADR 0009](0009-modules-and-the-graph.md) — the provision vocabulary a contribution belongs to.
-- [To-be 08 §3](../03-DESIGN/01-to-be/08-connectivity.md) — where exposure is specified.
diff --git a/02-DECISIONS/0109-a-package-registry-seat-is-one-per-ecosystem.md b/02-DECISIONS/0109-a-package-registry-seat-is-one-per-ecosystem.md
deleted file mode 100644
index 414abbaa..00000000
--- a/02-DECISIONS/0109-a-package-registry-seat-is-one-per-ecosystem.md
+++ /dev/null
@@ -1,106 +0,0 @@
----
-topic: the tiers
-status: accepted
-date: 2026-09-25
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0075-two-stores-and-which-provides-what.md
----
-
-# 109. A package registry seat is one per ecosystem, not one for all of them
-
-## Context
-
-Fixing `builder`'s consumption of `package-registry` tonight surfaced the shape ADR 0075 actually
-left implicit. 0075 split `artifact-store` from `package-registry` and said the second is "an
-ecosystem's own registry — npm, cargo, PyPI, Go" — but it defined one provision for all four,
-not one each.
-
-What that produces, read from the manifests as they stand:
-
-- `gitea`'s `module.json` declares `provides: package-registry` once, and its `serves` block
- carries exactly one path: `npm-path`. Nothing names a cargo or PyPI endpoint, though gitea's own
- package API serves both.
-- `builder` had, until tonight, a hand-written JSON fragment standing in for a real grant —
- `{"provision": "package-registry", "from": "gitea", "at": "127.0.0.1", ...}` — because nothing
- in the interface gave it a real one to ask for. The fragment named `npm-path` specifically; there
- was nowhere to put a second ecosystem's endpoint even if one had been wired.
-- The fix applied tonight declares `requires: ["artifact-store", "package-registry"]` and lets the
- mesh mint the grant properly — correct for what exists today, but it is one seat standing in for
- what should be several, the same conflation 0075 itself named and did not resolve for this
- provision specifically.
-
-**The version-skew problem 0075 wrote down for the artifact-store/package-registry split repeats
-one level down, inside "package registry" itself:** npm resolves by name and range from one
-namespace, cargo from another, and a single grant conflates them exactly the way one store for
-both digests and ranges would have.
-
-## Considered Options
-
-**1. One `package-registry` provision, gitea answers every ecosystem it can.** What exists today.
-Simplest to grant — one credential, one binding, done once per consumer. Rejected: a consumer
-that only ever needs npm still receives a grant shaped to cover cargo and PyPI, and there is no way
-to hand off *only* npm to a different provider (verdaccio, say) without renegotiating the whole
-provision for every consumer of any ecosystem.
-
-**2. One provision, parameterised by ecosystem.** `requires: package-registry` plus a declared
-`ecosystem: npm` alongside it, still one interface. Rejected: the `provides`/`requires` refusal
-mechanism this mesh already uses (two providers of one provision is a naming conflict until
-resolved) would need to become conditional on a parameter it does not otherwise carry anywhere in
-the mesh's resolution — a special case for exactly one provision, rather than the mesh's existing
-mechanism applied again.
-
-**3. One provision per ecosystem — `npm-package-registry`, `cargo-package-registry`,
-`docker-package-registry`, and so on, each independently `provides`/`requires`.** Chosen.
-
-## Decision
-
-**A package registry seat is one per ecosystem.** `npm-package-registry`, `cargo-package-registry`,
-`docker-package-registry` — each its own provision, resolved, granted, and refused exactly the way
-`artifact-store` and today's single `package-registry` already are. Adding an ecosystem is adding a
-provision, not widening one.
-
-**Gitea may hold several seats at once.** Nothing here says gitea answers only one; ADR 0075
-already established that a provider may answer more than one named thing on one machine ("a mesh
-running gitea for git and packages alongside a registry serving artifacts is an ordinary
-arrangement"). Gitea fulfilling `npm-package-registry` and `cargo-package-registry` both is the
-expected shape, not an exception.
-
-**Each seat's grant is independent.** A consumer that only needs npm holds only the
-`npm-package-registry` grant. Moving that one ecosystem to a different provider — verdaccio,
-named directly as the motivating case — means assigning `npm-package-registry` to verdaccio and
-leaving every other seat exactly where it was. No consumer of `cargo-package-registry` observes
-the change; no manifest naming `package-registry` broadly needs to be found and re-read.
-
-**`builder`'s fix tonight is the interim shape, not the target.** It correctly consumes the one
-seat that exists today (`package-registry`, npm in practice). Splitting it becomes, later,
-replacing that one line with the ecosystems `builder` actually uses — a manifest change, not a
-redesign of how `builder` asks for anything.
-
-## Consequences
-
-- `gitea`'s `module.json` gains a `provides` entry per ecosystem it actually serves, each with its
- own `serves` block (`npm-path`, a cargo path, a PyPI path) in place of the one `package-registry`
- entry with a single `npm-path` inside it.
-- `gitea`'s provisioner (`modules/gitea/provisioner/index.ts`) currently runs one `runProvisioner`
- registration for `package-registry`; each seat needs its own registration, or one provisioner
- keyed by which seat's `create`/`remove` fired — the mesh's `Provision` type does not yet carry
- which named provision a call is for when a module answers more than one, and that is worth
- checking before assuming the harness already supports it.
-- Every consumer's `requires` moves from the one name to however many ecosystems it actually uses.
- `builder` is the only known consumer today; widening later is one manifest line per module, not
- a migration.
-- `verdaccio`'s role sharpens: not "package-registry, an alternative for npm alone" (0075's phrasing)
- but a named `npm-package-registry` *provider*, a straight swap against gitea's answer to the same
- seat.
-- Not solved here: whether `cargo-package-registry` and `pypi-package-registry` are needed at all
- before something actually consumes them. This record names the shape; building unused seats is
- its own decision.
-
-## References
-
-- [ADR 0075](0075-two-stores-and-which-provides-what.md) — the record this extends; defined
- `package-registry` as the second provision without splitting it per ecosystem.
-- `mesh-catalog modules/builder/module.json` — tonight's fix, the interim single-seat shape.
-- `mesh-catalog modules/gitea/module.json`, `modules/gitea/provisioner/index.ts` — today's
- single-provision, npm-only implementation.
diff --git a/02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md b/02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md
deleted file mode 100644
index 42036ea2..00000000
--- a/02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md
+++ /dev/null
@@ -1,235 +0,0 @@
----
-topic: what runs on it
-status: accepted
-date: 2026-09-25
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0009-modules-and-the-graph.md
----
-
-# 110. A seat is held by one assignment, from a closed set, and it may deliver a provision
-
-> **Superseded in part — 2026-09-27, by [ADR 0122](0122-a-seat-is-data-a-rename-is-a-database-update.md).**
-> "The controller carries the set in code" no longer holds: the seats are rows of a table in the controller's
-> store, seeded by a migration, and a seat is referenced by a stable id, its name only a label. The closed set,
-> one holder per scope and a delivering seat as the single answer for its provision stand as decided here.
-
-> **Narrowed, not replaced — 2026-09-27, on merging two lines of work.** This was marked superseded by
-> [ADR 0126](0126-a-module-declares-its-own-seats.md), and that overstated it: 0126 says in as many
-> words that *"everything 0110 decided about what a seat is stands untouched"*. What moved is where the
-> set lives and who may add to it —
-> [0126](0126-a-module-declares-its-own-seats.md) lets a module declare one and makes the set derived,
-> [0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md) names the mesh's own
-> for their scope, and [0122](0122-a-seat-is-data-a-rename-is-a-database-update.md) moves them out of
-> code into a table. **What a seat *is* — one holder at its scope, a definition saying what a module
-> can hold against an assignment saying what it does, a role made singular rather than a module — is
-> this record and still current**, which is why those three rest on it.
-
-## Context
-
-[ADR 0009](0009-modules-and-the-graph.md) introduced claims: a module declares something
-singular, at a scope, and a second holder is refused. [ADR 0079](0079-the-foundation-seats-are-named-after-their-servers.md)
-named the foundation's three after their servers. That mechanism is enforced and works. What it
-means has drifted, and four things are now true of it that no record says.
-
-**Any well-formed name becomes a seat by being claimed.** The controller's manifest check
-refuses a claim only for a malformed name or an unknown scope. Nothing says which seats a mesh has.
-The names in use were each invented by the module that claims them: `the-showcase`,
-`the-build-machine`, `the-intrusion-prevention`.
-
-**Nothing can say what a mesh has, or who fills it.** There is no seat table and no command that
-lists seats. Holdings are assembled while planning, one node at a time, and discarded afterwards.
-The only way to answer "which seats does this mesh have, and which module holds each" is to read
-every manifest in two repositories, because the controller's own manifest lives in its own
-repository ([ADR 0069](0069-a-module-is-a-repository-and-a-path.md)), and then the controller's code,
-because one module it ships has its manifest composed there. While this record was being prepared,
-that enumeration was done by hand, and it missed both of the last two sources: eleven claims were
-reported where there are thirteen.
-
-**A claim in a definition makes a module singular, not a role.** The store module's definition
-claims `mesh-store`, so every assignment of it claims the seat, and a second store module on any other
-node is refused. What is singular is *the store the mesh itself uses*, not postgres. Any module can
-run on any node whose capabilities match, which is a core principle of the module system, and a claim
-written into the definition breaks it for every module that claims anything.
-
-**Some seats are the mesh's one of something everyone consumes, and nothing uses that fact.** A
-requirement for a mesh-scoped provision with more than one provider is refused until a person pins,
-**per consumer node**, which provider to use. [ADR 0109](0109-a-package-registry-seat-is-one-per-ecosystem.md)
-anticipates exactly that case, gitea and verdaccio both answering npm, and under today's resolution it
-would mean a pin on every machine that builds anything.
-
-## Considered Options
-
-**1. Leave seats as free-form exclusion, claimed in definitions.** Rejected. The overview stays
-unanswerable, a module that claims a seat can run on only one node, and a second provider of anything
-costs a pin per consumer node.
-
-**2. Two concepts: seats for exclusion, and a new word for the mesh's one consumable thing.**
-Rejected. Both mean "this mesh's one X". Every existing claim would first have to be classified into
-one or the other, and the overview a person wants is one list, not two.
-
-**3. A seat is held by one assignment, from a closed set, and holding it may deliver a provision.**
-Chosen.
-
-## Decision
-
-**The mesh defines a closed set of seats.** Each entry has a name, a scope, what holding it delivers
-(if anything), and the decision that made it a seat. A seat outside the set is refused wherever it
-is named. Adding a seat is a decision, for the same reason adding a shape to the host's vocabulary is
-one: the set is what a person reads to learn what a mesh can have, and a name added without an
-argument is a name nobody can explain later.
-
-**A definition says which seats a module *can* hold. An assignment says which it *does* hold.** The
-store module can hold `mesh-store`, and it may be assigned to every node. Exactly one of those
-assignments holds the seat, because that assignment said so. A second assignment saying so, at the
-seat's scope, is refused. So a seat makes a *role* singular, never a module, and moving the role is
-changing which assignment holds it, with no definition changed and nothing unassigned.
-
-**What the mesh knows about a seat's holder is what it knows about that assignment**: its node, the
-node's settings for it, and what it serves. Holdings are not stored separately. The seat points at an
-assignment, and a second record of the same fact would be a second thing to disagree with the first.
-
-**Holding a seat may deliver a provision.** A seat that delivers a provision may only be held by an
-assignment of a module that provides it, at the seat's scope.
-
-**A requirement may name a seat, and then the seat's holder answers it.** Naming the seat asks for
-*the mesh's* one, not for whichever provider is nearest, so the holder answers **even when another
-provider runs on the consumer's own node**, and nothing is asked of anyone. Unheld, the requirement is
-refused, naming the seat. A builder asks for the mesh's npm registry this way, and is served by the
-holder of `npm-package-registry` wherever it runs, with no pin on any machine.
-
-**A requirement that names no seat resolves as [ADR 0084](0084-which-provider-serves-a-consumer.md)
-has it**: a pin, then the provider on the consumer's own node, then the only provider. Where several
-remain and none is local, **a person chooses when the module is assigned**. Assignment lists the
-candidates, with the holder of a seat that delivers the provision suggested first, and records the
-answer on the assignment as its pin. Without an answer the module is not assigned. This keeps
-[ADR 0009](0009-modules-and-the-graph.md)'s rule that a requirement with several answers is never
-guessed. The choice is made either by the requirement naming the seat, or by a person at assignment,
-and never silently by what happens to run nearby. That is the failure
-[issue 106](../04-ISSUES/106-the-vault-claims-no-seat/00-report.md) names for the vault.
-
-**A seat delivers a provision only where the mesh has one answer for everyone.** The artifact store,
-the npm registry, git and the vault are each one per mesh by their own records, so their seats
-deliver them.
-
-**The foundation's seats deliver nothing.** `mesh-controller`, `mesh-store` and `mesh-broker` name
-which assignment the mesh *itself* uses: the controller, the store holding its records, the broker
-carrying its bus. The store and broker modules may run on other nodes too, and a database or `amqp`
-consumer that names no seat is served by co-location from whichever runs on its own node, the seat's
-holder included. Were `mesh-store` to deliver, a consumer could name it and be sent to the store the
-mesh keeps its own records in. That is not a store for consumers.
-
-**A seat may reserve its provision.** Where a second provider would break the reason the provision
-exists, only an assignment holding the seat may provide it at all: the parser refuses a definition
-that provides it without being able to hold the seat, resolution refuses an assignment providing it
-without holding the seat, and a pin cannot choose anyone else: a requirement for it always names the seat. `secret` is the one
-reserved provision.
-The vault is one per mesh because a second *"would be a second place to lose"*
-([ADR 0085](0085-a-secret-is-a-provision.md), as amended), and a second `secret` provider is exactly
-that, whether a pin chose it or not.
-
-**Seats are also informational.** The controller lists every seat in the set, what it delivers, and
-which assignment holds it, including seats nobody holds. An unheld seat is an answer, "this mesh has
-no X", not an error.
-
-**The first set is the twelve seats already claimed, plus two.** Thirteen claims are in use, and they
-name twelve seats because two alternative modules claim `the-resolver-configuration`. This record
-admits every seat the catalogue and the controller claim today, so no definition is refused by it:
-
-| seat | scope | delivers | can be held by | made a seat by |
-|---|---|---|---|---|
-| `mesh-controller` | mesh | — | `mesh-controller` | [0079](0079-the-foundation-seats-are-named-after-their-servers.md) |
-| `mesh-store` | mesh | — | `postgres` | [0079](0079-the-foundation-seats-are-named-after-their-servers.md) |
-| `mesh-broker` | mesh | — | `lavinmq` | [0079](0079-the-foundation-seats-are-named-after-their-servers.md) |
-| `mesh-vault` | mesh | `secret`, reserved | `mesh-vault` | this record, for [issue 106](../04-ISSUES/106-the-vault-claims-no-seat/00-report.md) |
-| `the-artifact-store` | mesh | `artifact-store` | `distribution` | [0075](0075-two-stores-and-which-provides-what.md) |
-| `the-catalogue` | mesh | — | `mesh-catalog` | this record |
-| `npm-package-registry` | mesh | `npm-package-registry` | `gitea` | [0109](0109-a-package-registry-seat-is-one-per-ecosystem.md) |
-| `the-build-machine` | node | — | `builder` | this record |
-| `the-dns-port` | node | — | `dnsmasq` | this record |
-| `the-intrusion-prevention` | node | — | `fail2ban` | this record |
-| `the-packet-filter` | node | — | `nftables` | this record |
-| `the-private-network` | node | — | the controller's private-network module | this record |
-| `the-resolver-configuration` | node | — | `resolv-conf` or `resolved-split-dns` | this record |
-| `the-showcase` | node | — | `showcase` | this record |
-
-There are two additions. `npm-package-registry` is [ADR 0109](0109-a-package-registry-seat-is-one-per-ecosystem.md)'s
-seat, named after the provision it delivers, as 0079 named the foundation's seats after what they are.
-A gitea assignment holds it. verdaccio provides the same provision and cannot hold the seat, so it is
-the second provider this record exists to make harmless. Moving npm to it would take a definition
-saying it can hold the seat, and then an assignment saying it does.
-
-`mesh-vault` answers [issue 106](../04-ISSUES/106-the-vault-claims-no-seat/00-report.md). The vault is one
-per mesh ([ADR 0085](0085-a-secret-is-a-provision.md), as amended), and until now that was enforced by
-nothing. The seat is named after its server, by the 0079 convention.
-
-`the-dns-port` is listed as delivering nothing, although `dnsmasq` provides `wildcard-resolution`.
-That provision is node-scoped and answered on the machine, so no preference between providers arises.
-
-## What this changes in earlier records
-
-On acceptance, each of these is amended by this record, not edited:
-
-- [ADR 0009](0009-modules-and-the-graph.md): a claim in a definition says a module *can* hold a seat;
- the assignment says it does.
-- [ADR 0079](0079-the-foundation-seats-are-named-after-their-servers.md) and
- [ADR 0078](0078-the-store-and-broker-are-modules.md): "a mesh runs one postgres and one
- lavinmq" becomes one holder of `mesh-store` and one of `mesh-broker`. The store and broker modules may
- run on other nodes.
-- [ADR 0084](0084-which-provider-serves-a-consumer.md): a requirement may name a seat, which its holder
- answers; and where several providers remain and none is local, the choice is asked when the module is
- assigned and recorded as a pin, rather than refused until someone pins it.
-- [ADR 0109](0109-a-package-registry-seat-is-one-per-ecosystem.md): one provision per package
- ecosystem stands. Where 0109 says *seat*, it means that provision. Only `npm-package-registry` is
- also a seat in this set. A cargo or docker registry becomes one by a record, as any seat does.
- "Gitea may hold several seats" reads: gitea may provide several ecosystems, and hold the seat of
- each one that is a seat. Moving npm to verdaccio is not "assigning `npm-package-registry` to
- verdaccio". It takes verdaccio's definition saying it can hold the seat, and then an assignment
- holding it.
-- [To-be 23](../03-DESIGN/01-to-be/23-choosing-a-provider.md): the same two changes, in the design
- that describes choosing a provider.
-- [To-be 21](../03-DESIGN/01-to-be/21-the-installation-in-full.md): genesis assigns the foundation's
- store, broker and controller holding their seats, where their definitions claim them today.
-
-## Consequences
-
-- The controller carries the set in code. A test asserts its size, and that every entry names the
- record that made it a seat, so changing the set means finding the argument rather than a number.
-- An assignment gains the seats it holds. Genesis assigns the foundation's store, broker and
- controller holding their seats, where today their definitions claim them.
-- Manifest validation refuses an unknown seat, a seat named at the wrong scope, and a delivering seat
- named by a module that does not provide the provision. Resolution refuses a second holder, and an
- assignment holding a seat its module cannot hold.
-- Resolution answers a requirement naming a seat with its holder. Assignment asks a person where
- several providers remain, suggesting the seat's holder first, and records the answer as a pin. A
- provider record gains the module it came from.
-- A `seats` command lists the set with each seat's holder, derived from assignments.
-- **What got harder:** a module wanting a new singular role can no longer invent a name. It needs a
- record. And an assignment has one more thing to say. Both are the point.
-- **Not changed:** the controller's seat placeholder stays as it is. It exists so the controller can
- reach a foundation it made before any module existed.
-
-## How it is checked
-
-| Rule | Checked by |
-|---|---|
-| The set is closed, and every entry names its decision | A controller unit test asserts the set's size and a non-empty decision for every entry. |
-| A seat outside the set is refused | Manifest-validation tests for an unknown seat, the wrong scope, and a delivering seat named by a module that does not provide it. |
-| Every module in use names a seat in the set | A controller test parses every catalogue manifest and fails on any refused seat. The lab's beds read the same manifests ([ADR 0089](0089-a-bed-reads-the-catalogue-it-proves.md)). |
-| A seat is held by one assignment, not by a module | A resolution test: the store module assigned to two nodes resolves, with one assignment holding `mesh-store`; a second assignment asking to hold it is refused. |
-| A requirement naming a seat is answered by its holder | Resolution tests: two providers with the seat held; a second provider on the consumer's own node, where the holder still answers; the seat unheld with two providers, and with **one** provider, both refused naming the seat. |
-| An assignment holds only a seat its module can hold | A resolution test: an assignment holding a seat its definition does not name is refused. |
-| Every seat is listed with its holder | A `seats` command test: every seat in the set is listed with its scope, what it delivers and its holder, and an unheld seat is listed as unheld. |
-| Several providers and none local is a person's choice | An assignment test: the candidates are listed with the delivering seat's holder first; the answer is recorded as a pin; with no answer the module is not assigned. |
-| The foundation's seats route nobody | A resolution test: with the store module on two nodes, a database consumer is served by the one on its own node, whichever holds `mesh-store`; a requirement naming `mesh-store` is refused, because it delivers nothing. |
-| A reserved provision has no other provider | The parser refuses a definition providing `secret` that cannot hold `mesh-vault`; resolution refuses an assignment providing it without holding the seat, and a pin on a `secret` requirement. |
-
-## References
-
-- [ADR 0009](0009-modules-and-the-graph.md): claims, scopes, and "refused, never guessed"
-- [ADR 0079](0079-the-foundation-seats-are-named-after-their-servers.md): a seat named after what it is
-- [ADR 0109](0109-a-package-registry-seat-is-one-per-ecosystem.md): the per-ecosystem registry seats
-- [ADR 0084](0084-which-provider-serves-a-consumer.md), [to-be 23](../03-DESIGN/01-to-be/23-choosing-a-provider.md):
- pins, co-location and refusal
-- `mesh-controller internal/catalogue/resolve.go` (`checkClaims`, the brokered branch of `Resolve`),
- `internal/catalogue/manifest.go` (claim validation), `cmd/mesh-controller/plan.go` (holdings)
diff --git a/02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md b/02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md
deleted file mode 100644
index 7e577fa3..00000000
--- a/02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md
+++ /dev/null
@@ -1,105 +0,0 @@
----
-topic: building it
-status: accepted
-date: 2026-09-25
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0069-a-module-is-a-repository-and-a-path.md
----
-
-# 111. A build source is on the mesh's git seat, or it is an external repository
-
-## Context
-
-[ADR 0069](0069-a-module-is-a-repository-and-a-path.md) made a module a repository, a path and a
-ref, and the controller records all three against the module so it can rebuild it and say when
-its source has moved ahead. **The repository is recorded exactly as a person typed it.** `build
-` hands the string to a build machine, which runs `git clone` on it, and the same string
-becomes the module's recorded source.
-
-**So a self-hosted forge's address is written into every module built from it.** The mesh runs its
-own forge, and most of what it builds lives there. Every one of those modules carries the forge's
-scheme, host and port in its recorded source. Move the forge to another machine, or change the port
-it is published on, and every recorded source is stale at once. Nothing notices until a rebuild fails
-to clone.
-
-**And nothing names the mesh's git at all.** gitea serves git over HTTP and over SSH, and the mesh's
-vocabulary contains neither. No provision, no `serves`, no seat, as the forge survey
-([research 013](../01-RESEARCH/013-the-forge-and-the-registries/the-survey.md)) found. The only trace
-is a label on its public route, which the mesh is explicitly not meant to interpret.
-
-**External repositories are ordinary, and must stay so.** An application the mesh hosts may live on a
-public forge. Building it from its URL works today and must keep working unchanged.
-
-## Considered Options
-
-**1. Keep recording literal URLs.** Rejected. It is the problem: the forge's address copied into
-every module built from it.
-
-**2. Recognise a self-hosted source by matching its URL against the forge's current address.**
-Rejected. It infers the kind of source from the shape of a string, and the inference fails in the
-one case it exists for: after the forge moves, old URLs no longer match anything.
-
-**3. Two explicit forms: a repository on the holder of the `git` seat, or an external URL.** Chosen.
-
-## Decision
-
-**The mesh has a `git` seat.** It is mesh-scoped and delivers the `git` provision, per
-[ADR 0110](0110-a-seat-is-a-module-assignment-from-a-closed-set.md). Its holder provides `git`,
-serving how a repository on it is cloned: the scheme and the port. A gitea assignment holds it.
-
-**A source is on the git seat, or it is external, and the mesh records which.**
-
-- `build --self /` builds from a repository on the seat's holder. The recorded
- source is the repository's path on that holder, and the seat it is on. **It never contains an
- address.** At the moment of building, the controller composes the clone URL from where the
- holder runs and what it serves for `git`, so a moved forge changes nothing recorded.
-- `build ` is unchanged: an external repository, recorded and cloned exactly as given. GitHub
- and GitLab are the ordinary cases.
-
-**An unheld seat refuses self-hosted builds and nothing else.** With nobody holding `git`, `build
---self` is refused, naming the seat and saying what would hold it. External builds are unaffected. A
-mesh without a forge of its own builds from external repositories only, and says so rather than
-failing to clone.
-
-**The build machine is not told the difference.** It receives a URL either way. Composing the URL is
-the controller's job, because only the controller knows where the seat's holder runs.
-
-## What this changes in earlier records
-
-On acceptance, each of these is amended by this record, not edited:
-
-- [ADR 0069](0069-a-module-is-a-repository-and-a-path.md): a module's repository is recorded either as
- a path on the `git` seat or as an external URL, never as an address of the mesh's own forge.
-- [ADR 0110](0110-a-seat-is-a-module-assignment-from-a-closed-set.md): the closed set gains `git`,
- mesh-scoped, delivering `git`, held by a gitea assignment.
-
-## Consequences
-
-- The controller's inventory gains a column saying which seat a source is on. It is empty for
- every module recorded before this, which is correct: they were all recorded as literal URLs.
-- `build`, `build --behind` and `build --dry-run` resolve a seat source before asking a builder. The
- recorded source keeps the seat form; the build log keeps the URL that was actually cloned, because
- that is what happened.
-- gitea can hold `git` and provides it, serving HTTP clone on its web port; the forge's assignment holds the seat.
-- **Not decided: credentials for private repositories.** The mesh's own repositories are public, and
- clone without one. A private repository still works only if the build machine's own git
- configuration authenticates, exactly as before. Delivering a clone credential through the `git`
- provision's grant is the obvious next step, and it is its own decision.
-- **Not changed:** modules already recorded from the forge keep their literal URLs until they are
- rebuilt with `--self`. Rewriting them in place would be the URL-matching this record rejects.
-
-## How it is checked
-
-| Rule | Checked by |
-|---|---|
-| A seat source records no address | A controller test resolves a seat source and asserts the recorded repository is the path alone. |
-| The URL comes from the holder | A test composes the clone URL from a holder's node address and served `git` scheme and port, and a second where the port was moved on the node. |
-| An unheld seat refuses self-hosted builds only | A test with no holder: `--self` is refused naming the seat; an external URL passes through unchanged. |
-
-## References
-
-- [ADR 0069](0069-a-module-is-a-repository-and-a-path.md): a module is a repository, a path and a ref
-- [ADR 0110](0110-a-seat-is-a-module-assignment-from-a-closed-set.md): seats, and a seat delivering a provision
-- [Research 013](../01-RESEARCH/013-the-forge-and-the-registries/the-survey.md): git is served and declared nowhere
-- `mesh-controller cmd/mesh-controller/build.go`, `internal/builder/builder.go`
diff --git a/02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md b/02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
deleted file mode 100644
index f5890a26..00000000
--- a/02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
+++ /dev/null
@@ -1,192 +0,0 @@
----
-topic: what runs on it
-status: accepted
-date: 2026-09-25
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0046-a-module-configuration-is-its-assignments-not-its-manifest.md
----
-
-# 112. A module definition names no node, no mesh and no path: everything it needs is a requirement the mesh resolves
-
-## Context
-
-[Issue 119](../04-ISSUES/119-a-module-definition-decides-where-its-files-live/00-report.md) found
-**789 host-path strings in 70 of the catalogue's 71 module definitions.** Each definition chooses
-where on the machine its directories, mounts, bindings, secrets, env-files and received files live,
-and often repeats that path in an environment variable or in code. Mounts are checked against what
-the definition declares ([ADR 0091](0091-a-mount-is-declared-three-ways.md)); nothing checks the
-copies. The issue records what that has already allowed:
-
-- a provider that would provision nobody without a word;
-- a contributions file that carries host paths into containers, so every provider must mount its
- grants directory at the identical path;
-- defaults in code that disagree with their own manifests;
-- every identity keyed by the module's name, which is why one module cannot be assigned to one node
- twice. This record keeps that, and says so below.
-
-**Paths are one case of a wider pattern.** A module gets what it needs through at least six separate
-mechanisms today, each with its own syntax and its own failure modes:
-
-- provisions, read through bindings;
-- settings on the assignment ([ADR 0046](0046-a-module-configuration-is-its-assignments-not-its-manifest.md));
-- ports the mesh assigns ([ADR 0038](0038-the-mesh-assigns-the-port.md));
-- machine facts a manifest asks for;
-- secrets, either minted or accepted from an operator;
-- literals carried in the definition itself.
-
-The mesh has already unified parts of this. Ports became the mesh's rather than the module's (0038),
-configuration became the assignment's (0046), and which provider serves a consumer became the
-assignment's choice ([ADR 0084](0084-which-provider-serves-a-consumer.md)). What remains is the
-concept that joins them.
-
-## Considered Options
-
-**1. Keep host paths in definitions, and check that every copy agrees.** Rejected. It checks the
-agreement of something that should not be there. A definition still could not follow its data to
-another disk, or be adopted onto a machine whose data is already somewhere.
-
-**2. Keep the separate mechanisms, and add directories as a seventh.** Rejected. It fixes paths and
-keeps the pattern that produced them: each mechanism is resolved, validated and refused differently,
-so a module author learns six systems and a reviewer checks six kinds of gap.
-
-**3. One concept: a module requires, and the mesh resolves every requirement against a contract.**
-Chosen.
-
-## Decision
-
-**A module definition is node-agnostic and mesh-agnostic.** It names no node, no mesh and no host
-path. **Everything a module needs is a requirement**: a name, a contract saying what the module may
-read from it, and which kind of provider answers it.
-
-**Installing a module on a node resolves every requirement, or refuses.** A refusal names each
-unresolved requirement and what could answer it, all at once.
-
-**There are four kinds of provider, and the set is closed:**
-
-| provider | answers | today's mechanism it replaces |
-|---|---|---|
-| **another module** | a database, a bucket, a vhost, a secret, a route | provisions and bindings |
-| **the node's host** | a directory, a port, facts about the machine | resource paths, `${port:}`, `${machine:}`, facts |
-| **the mesh** | the module's identity and names, and the delivery of every answer | derived logins and generated names; the controller's delivery |
-| **the operator, through the assignment** | a value a person chooses that is not secret: a public name, a greeting, a number of workers | settings, carried literals |
-
-A module provider is chosen as [ADR 0084](0084-which-provider-serves-a-consumer.md) and
-[ADR 0110](0110-a-seat-is-a-module-assignment-from-a-closed-set.md) say. A requirement naming a seat is
-answered by its holder. Otherwise it is a pin, then co-location, then the only provider, and where
-several remain, a person chooses at assignment and the choice is recorded as a pin.
-A host provider is always the module's own node, because a host path or a port means nothing on any
-other. An operator value is the assignment's, or the requirement's default, or unresolved.
-
-**A person's value stays cheap.** An operator requirement's contract is a type and, optionally, a
-default. It needs no provider module, no grant and no credential.
-
-**Every shared secret is a `secret` requirement, answered by the vault**, with no exception by kind
-([ADR 0113](0113-the-vault-makes-every-secret.md)). A private key is made where it is used and is not
-a requirement. An external API key an operator chooses is no
-different: the operator delivers it to the vault ([ADR 0092](0092-an-operator-delivers-a-pair-credential.md)),
-and the module requires a `secret` like any other. A provider that needs a secret for a consumer
-requires it from the vault, like any consumer, and answers with resources and data. The mesh carries
-every answer back.
-
-**A directory is a host provision.** Its contract is the owner and mode the module needs, including
-the owner its image expects ([ADR 0107](0107-persistent-data-is-a-directory-bind-never-a-named-volume.md)).
-It carries no persistence flag. A directory is kept while it holds anything
-([ADR 0030](0030-data-outlives-the-mesh-that-declared-it.md), which refused a `keep` flag for good
-reason), and data that is disposable is not a directory but a named volume (0107). *Where* it is on
-the machine is the assignment's. A node has a default layout, and an assignment may place a directory
-elsewhere: on a second disk, or where an adopted machine's data already is
-([ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md)).
-
-**An operator's shared data stays an `access`** ([ADR 0051](0051-shared-data-is-the-operators.md)),
-not a directory. 0051 rejected giving a directory an operator owner, because the mesh must never
-create, chown or remove such data, and that stands. Only where its location is written changes: the
-module requires read or read-write access, and the assignment says where the data is.
-
-**Inside a container, a module sees its own paths.** The definition says where the image expects each
-directory. The mesh mounts the assignment's location there. No host path is ever a value a process
-reads, and the mesh's own files (answers, contributions) name nothing by host path, so a provider needs
-no mount at a machine-identical path.
-
-**A module is assigned at most once to a node.** An assignment is a module on a node, and that pair is
-its identity: its directories, containers, login, broker account and settings are keyed by it, as
-they are today. A module may run on many nodes, and one of those assignments may hold a seat
-([ADR 0110](0110-a-seat-is-a-module-assignment-from-a-closed-set.md)). Running the same module twice
-on one machine is not supported. The cases that seemed to need it, such as two stores of one engine or
-two stages of one application, are different modules, or the same module on different machines. The
-line is drawn because every identity in the mesh is already a module on a node, and a second
-instance would have to rename all of them.
-
-**What must stay singular stays so** by a seat, or by an operator value colliding: a public name
-already held by another assignment is refused like any other singular thing.
-
-**The foundation's first secrets are delivered, then adopted.** Genesis generates them before the vault
-can run and hands them to the vault once it is installed, and from then on they are answered the same
-way as every other secret ([ADR 0113](0113-the-vault-makes-every-secret.md)).
-
-## What this changes in earlier records
-
-On acceptance, each of these is amended by this record, not edited:
-
-- [ADR 0046](0046-a-module-configuration-is-its-assignments-not-its-manifest.md): settings become
- operator requirements on an assignment.
-- [ADR 0038](0038-the-mesh-assigns-the-port.md): a port becomes a host requirement. What 0038 decided
- is unchanged; it is the first case of this rule.
-- [ADR 0051](0051-shared-data-is-the-operators.md): an access keeps its shape and its semantics; its
- path moves from the definition to the assignment.
-- [ADR 0091](0091-a-mount-is-declared-three-ways.md): a mount's host side is a resolved requirement,
- checked as resolved rather than as a path the definition declares.
-- [To-be 21](../03-DESIGN/01-to-be/21-the-installation-in-full.md): the step that builds and runs a
- store module as a database provider, beside the foundation's store on the same node, would run the
- store module twice on one node. The adopted store module ([ADR 0078](0078-the-store-and-broker-are-modules.md))
- holds `mesh-store` and serves that node's database consumers by co-location, so there is no second
- one.
-- The [glossary](../00-META/glossary.md): *provision* widens from "a service one module provides" to a
- requirement answered by any of the four providers, and *requirement* and *contract* are added. None
- of it lands while this record is only proposed, because the glossary is the authority on the words
- in use, not on words under review.
-
-## Consequences
-
-- **Every definition changes.** 70 of 71 name host paths today, and most use at least three of the
- mechanisms this replaces. The change is mechanical for most. The design has to say how existing
- modules move without their data moving: an adopted or already-running assignment is placed where
- its data already is.
-- The controller resolves every requirement at assignment and refuses unresolved ones. The host
- answers directories and ports. The settings, placeholders, facts and bindings that exist today
- are retired as separate mechanisms, once nothing uses them.
-- Identity stays a module on a node. Nothing is renamed, and a login still fits the tightest backend
- as [ADR 0049](0049-a-consumers-identity-fits-the-tightest-backend.md) arranges.
-- **What got harder:** one module cannot run twice on one machine; a second stage or a second store
- of one engine is a different module or a different machine. And a definition no longer says where
- a module's data is on a machine, or what a setting's value is. The assignment does, and `plan` shows
- it. That is the point, and it is also a real loss of at-a-glance legibility, which the overview has
- to give back.
-- **Not decided here:** the syntax a definition reads a requirement's fields with; a node's default
- layout; the order in which the mechanisms are retired. [To-be 27](../03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md)
- proposes all three.
-
-## How it is checked
-
-| Rule | Checked by |
-|---|---|
-| A definition names no host path | A catalogue test: the host side of every mount, and every resource location, is a requirement rather than an absolute path. A declared list of exceptions shrinks to empty as definitions move. |
-| No host path is a value a process reads | A catalogue test: every absolute path in a container's environment or env-files lies on the container side of one of its mounts, or is declared the image's own. A second test finds literal paths in module code used as fallbacks for an environment variable. |
-| A definition names no node and no mesh | The parser has no field that names a node; a node is named only in an assignment. A catalogue test finds no domain name in any definition value. |
-| Every requirement has one of the four providers | The parser refuses a requirement whose provider kind is not one of the four. |
-| Installation resolves every requirement | A resolution test with one requirement unanswered: refused, naming it and what could answer it. |
-| A host requirement is answered on its module's own node | A resolution test: an assignment placing a directory or a port on another node is refused. |
-| A module is assigned at most once to a node | A resolution test: assigning a module to a node that already runs it is refused, naming the existing assignment. |
-| A public name already taken is refused | A resolution test: a second assignment asking for a public name another holds is refused, naming the holder. |
-| An adopted assignment is placed where its data is | An adoption test: the directory resolves to the data's existing location, and nothing is moved. |
-
-## References
-
-- [Issue 119](../04-ISSUES/119-a-module-definition-decides-where-its-files-live/00-report.md): the evidence
-- [ADR 0038](0038-the-mesh-assigns-the-port.md), [ADR 0046](0046-a-module-configuration-is-its-assignments-not-its-manifest.md),
- [ADR 0084](0084-which-provider-serves-a-consumer.md): the parts already unified
-- [ADR 0110](0110-a-seat-is-a-module-assignment-from-a-closed-set.md): which module provider answers
-- [ADR 0113](0113-the-vault-makes-every-secret.md): the vault makes every secret, and how answers travel
-- [ADR 0092](0092-an-operator-delivers-a-pair-credential.md): the operator as a provider
-- [ADR 0051](0051-shared-data-is-the-operators.md), [ADR 0107](0107-persistent-data-is-a-directory-bind-never-a-named-volume.md),
- [ADR 0030](0030-data-outlives-the-mesh-that-declared-it.md): what a directory's contract carries, and what it must not
diff --git a/02-DECISIONS/0113-the-vault-makes-every-secret.md b/02-DECISIONS/0113-the-vault-makes-every-secret.md
deleted file mode 100644
index f0a70784..00000000
--- a/02-DECISIONS/0113-the-vault-makes-every-secret.md
+++ /dev/null
@@ -1,304 +0,0 @@
----
-topic: what runs on it
-status: accepted
-date: 2026-09-25
-deciders: jochen
-reconstructed: false
----
-
-# 113. The vault makes every shared secret, a provider makes resources and data, and the mesh carries both
-
-## Context
-
-**A shared secret comes into being many different ways today**, counted across the catalogue and the
-controller on 2026-09-25:
-
-| kind | made by | used by |
-|---|---|---|
-| a credential between a consumer and a provider | the controller | 16 modules, and 3 more for model access, counted below |
-| a module's own secret (`own-secrets`) | the controller, as a random value nothing owns | 54 modules |
-| a module's broker account | the controller, but only when a person runs a separate command; otherwise the random value above, which cannot work ([issue 095](../04-ISSUES/095-a-module-assigned-after-genesis-has-no-broker-account/00-report.md)) | 49 modules |
-| a node's and the builder's broker accounts | the controller, each in its own code path | every node, the builder |
-| an enrolment token | the controller | every node joining |
-| a `secret` from the vault | the controller mints it, and the vault only records it ([ADR 0085](0085-a-secret-is-a-provision.md), as amended) | 6 modules |
-| a value an operator accepts | a person ([ADR 0092](0092-an-operator-delivers-a-pair-credential.md)) | where accepted |
-| a licence for model access | a separate controller context with its own store | model consumers |
-| the foundation's root secrets | genesis, sealed to the operator key | the foundation |
-
-**The vault was built to end the second row, and did not.** ADR 0085 says a module's own secret
-*"stops being a generated value that nothing owns"*. 54 modules still use one, and 6 use the vault.
-The replacement was added and the old path was never retired.
-
-**ADR 0085 considered and rejected making the vault the only maker**, because *"the controller must
-mint in order to deliver any provision — the vault's own credential among them"*: the vault cannot
-make the credentials that exist before it does. That objection is real, and this record has to answer
-it rather than step around it.
-
-**Rotation has gaps.** To-be 13 makes rotation one command, all-or-nothing, with a stated window in
-which a consumer cannot authenticate. A consumer restarts only if its definition remembered to say so;
-a container fed by an env-file was not recreated when that file changed
-([issue 103](../04-ISSUES/103-a-container-is-not-recreated-when-a-file-it-reads-changes/00-report.md),
-since fixed in the host); and some secrets are read only when a service first initialises, where a restart changes nothing.
-
-**And providers cannot answer with data.** [ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md)
-left *"delivering provider-generated data back to a consumer"* to a separate decision. The analytics
-provider's site id and the DNS provider's record have no way back, and say so in their code.
-
-## Considered Options
-
-**1. Keep the controller minting, and tidy the paths.** Rejected. The paths are the problem: each is
-made, kept, rotated and audited differently, and tidying keeps them all.
-
-**2. Every provider mints its own secrets, with one shared function in the SDK.** Rejected. Generation
-becomes uniform, but custody stays spread over every provider's machine, so rotation, audit and the
-operator's break-glass copies cover only some secrets. Each SDK language needs its own implementation.
-
-**3. Raise the vault first at genesis, so it makes even the first secrets.** Rejected. The vault is
-built on the shared runtime base, which the installation makes only after the store, the broker and
-the controller exist, and the vault learns what to answer from the controller over the bus. Running
-it first means reordering the whole installation and giving the vault a second way of being asked.
-
-**4. The vault makes every shared secret; genesis delivers the first ones to it.** Chosen. It answers
-0085's objection with a mechanism the mesh already has: a value delivered to the vault.
-
-## Decision
-
-**There are two kinds of secret, and each has one rule.**
-
-- **A shared secret** is a value more than one party must hold: a password, a token, an API key. **The
- vault makes every one.** Nothing else in the mesh generates a shared secret.
-- **A private key** is made where it is used and never leaves: a node's sealing key, the operator's
- key, the mesh's certificate authority. This is not a second way of making secrets. A private key any
- other party ever held would no longer be private.
-
-**Every shared secret is a `secret` requirement, answered by the vault:**
-
-- a **credential between a consumer and a provider**. A provision's contract declares *for each
- consumer, one secret*, and resolution expands it into one requirement per consumer. So gitea
- requiring a database makes the database's provider require a secret for gitea, and the vault
- answers it. The provider's own code does not change: it is handed a login and a password, as today;
-- a module's **own secret**. `own-secrets` is retired;
-- every **broker account** on the mesh's bus: a module's, a node's host's, the builder's, the
- controller's. The broker holding `mesh-broker` carries the mesh's bus
- ([ADR 0110](0110-a-seat-is-a-module-assignment-from-a-closed-set.md)), and its own provisioner creates
- each account from the vault's secret, like any provider. The controller no longer creates accounts,
- and there is no separate command to forget;
-- an **enrolment token**. The vault makes it; the operator receives the token, sealed to the operator
- key, to hand to the joining machine; the controller receives only what it needs to verify it, never
- the token itself;
-- a **secret operator value**, such as an external API key, which the operator delivers to the vault
- ([ADR 0092](0092-an-operator-delivers-a-pair-credential.md)). A licence's credential is one of these.
- What the licences context adds, refreshing a token, is provider behaviour, decided in its own record;
-- a **secret a backend issues itself**, such as an API token a forge hands out exactly once when asked.
- The vault cannot make that value. The module that received it delivers it to the vault, which keeps
- it and provides it like any other; rotating it means asking the backend again.
-
-**The controller is a module, and takes the same path.** Its store logins (inventory, identity and
-licences) and its bus accounts are own secrets of its definition today, and become `secret` requirements
-of that definition like any module's. **A node's host is the one party with no definition.** Its bus
-account is a requirement the mesh makes for each enrolled node, answered by the vault, sealed to that
-node and carried like any other. It is the only requirement not written in a definition, because the
-host is what runs definitions.
-
-**Only the vault may provide `secret`.** An assignment providing it must hold the `mesh-vault` seat.
-The parser refuses a definition that provides it and cannot hold the seat, and a pin cannot route a
-`secret` requirement anywhere else, because there is nowhere else.
-
-**A secret has recipients, and the vault delivers to each.** The database credential has two: the
-provider, which *applies* it by creating the login, and the consumer, which *reads* it and presents it
-when it connects. The vault hands the value to the mesh sealed to each recipient's node. The controller
-and the broker carry sealed values they cannot open.
-
-**Genesis delivers, and the vault adopts.** The vault is built on the shared runtime base, which the
-installation makes only after the store, the broker and the controller are running
-([to-be 21](../03-DESIGN/01-to-be/21-the-installation-in-full.md)). So the vault is installed **as soon
-as that base exists**, before any other module built on it, and everything needed before that moment is
-generated by genesis:
-
-- the store's superuser, and the broker's admin in the hashed form the broker needs;
-- the bus accounts of the temporary and permanent controller (its account and the broker-management
- login), the control-node's host, the builder, the broker's own provisioner and the vault;
-- the controller's three store logins (inventory, identity and licences), and the first enrolment
- token.
-
-Until the broker's provisioner runs, genesis creates the bus accounts it generated, with the broker's
-admin, as the controller does today. Genesis seals each value twice: to the control-node's key, so that when the
-vault is installed the controller **delivers the values to the vault, recorded as the mesh's own**, not
-as an operator's, with nobody present; and to the operator key, as the break-glass copy
-[ADR 0085](0085-a-secret-is-a-provision.md) keeps of every root secret. The first enrolment token reaches
-the operator the same way.
-That distinction matters: an operator's value is never replaced ([ADR 0092](0092-an-operator-delivers-a-pair-credential.md)),
-and these are, because the vault can make their replacements. The broker's provisioner then adopts the
-accounts genesis created. From then on the vault makes every shared secret, and genesis has made its
-last one.
-
-**Raising the vault or the broker again is a genesis act.** Moving the `mesh-vault` or `mesh-broker`
-seat to a new assignment, or recovering either after it is lost, is done the way genesis did it: the
-values it needs are delivered, not made by a vault that is not there. They come from the operator-sealed
-copies, which the operator opens. The vault keeps a copy of every secret sealed to the operator key
-(0085), so nothing the mesh relies on exists only inside the vault. That is a break-glass procedure,
-stated and checked, never an ordinary assignment.
-
-**A provider makes resources and data, and the mesh carries data back.** A provider's adapter may
-answer with its contract's non-secret fields: a site id, a registered name. The mesh delivers them to
-the consumer as resolved values. Who a consumer is stays the mesh's: a provider makes what a consumer
-is *given*, never what it is *called* ([ADR 0049](0049-a-consumers-identity-fits-the-tightest-backend.md)).
-
-### Rotation
-
-**Who asks and who makes are decided here; the mechanism is not.** A rotation is asked of the vault,
-by an operator or by the vault's policy, such as a maximum age in the requirement's contract, and the
-vault makes the new value. A delivered value the vault cannot replace, such as an external API key, is
-not rotated by the vault: rotating it means an operator delivering a new one. A secret a backend
-issued is rotated by the module that holds the backend asking it again and delivering the new value to
-the vault.
-
-**Each recipient takes a new value one of two ways, marked per recipient:**
-
-| recipient takes it by | example | what happens on rotation |
-|---|---|---|
-| **applying** it | a provider setting a login's password; the broker's provisioner updating an account; a store's provisioner changing its own superuser | its provisioner applies the new value; it is never restarted for it |
-| **reading it at start** | a consumer reading its password when it starts | the host recreates it, because a file it read at creation changed |
-
-The marking is per recipient, not per secret, because one secret has recipients of both kinds. A
-provision's contract marks its provider's side, which applies. A consumer's side is read at start
-unless its requirement says otherwise. The broker's contract marks the host's bus account the same
-way: the broker's provisioner applies it, and the host reads it.
-Every module in the catalogue reads its secrets at start, and none watches them
-([research 016](../01-RESEARCH/016-how-a-credential-can-be-rotated/02-the-readers.md)). A secret a
-backend takes only when it first initialises is marked applied, and its provider's provisioner makes
-the change using the old value. Where no provisioner can make it, the requirement is marked **not
-rotatable by the mesh**, and a rotation request is refused, saying why, rather than restarting a service
-that would carry on with the old value.
-
-**How old and new change over is decided in [ADR
-0114](0114-a-shared-credential-rotates-over-two-credentials.md).** Three mechanisms were measured against
-every provider's code in [research
-016](../01-RESEARCH/016-how-a-credential-can-be-rotated/00-overview.md): in place, as the controller's
-`rotate` does today; two secrets on one login; and two logins over one resource. Its findings bound the
-choice:
-
-- every credential provider already re-applies a password in place, so today's rotation works, with a
- window in which a consumer cannot authenticate;
-- eight of nine name the consumer's resource after its login, and five destroy the consumer's data when
- they remove the login. **No mechanism may retire a login through today's remove**, because in those
- five it deletes the consumer's data;
-- one backend holds two passwords on one login, and two more hold several tokens;
-- every provider can hold two credentials over one resource, eight as two logins and one as two tokens,
- once the adapter separates the resource from the credential.
-
-On those facts, 0114 rotates a credential two parties hold over two credentials, rotates one a single
-party holds in place, staged, and separates retiring a credential from removing a consumer.
-
-## What this changes in earlier records
-
-On acceptance, each of these is superseded or amended by this record, not edited:
-
-- [ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md) is superseded: the controller
- no longer mints a provider's credential; the vault makes it. That a provider is handed its
- credential and seals nothing stands.
-- [ADR 0085](0085-a-secret-is-a-provision.md) is amended: the vault makes every shared secret, own
- secrets are retired, and its rejection of vault-only minting is answered by genesis delivering the
- first secrets. Genesis seals its values to the control-node's key as well as to the operator key, so
- the controller can deliver them unattended. "The vault stores no plaintext, ever" and the
- operator-sealed break-glass copies stand.
-- [ADR 0092](0092-an-operator-delivers-a-pair-credential.md) is amended: an operator delivers a secret
- to the vault. Genesis's values reach the vault by delivery too, but are recorded as the mesh's own,
- so 0092's rule that an operator's value is never replaced does not apply to them.
-- [ADR 0043](0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md) is amended: a broker
- account is created by the broker's provisioner, not the controller. Its scoping stands.
-- [To-be 13](../03-DESIGN/01-to-be/13-credentials-and-their-rotation.md),
- [to-be 21](../03-DESIGN/01-to-be/21-the-installation-in-full.md) and
- [to-be 24](../03-DESIGN/01-to-be/24-the-secrets-vault.md) are amended: the vault makes a rotated value and each
- requirement says whether its recipient applies it or reads it at start, and the changeover is
- [ADR 0114](0114-a-shared-credential-rotates-over-two-credentials.md)'s; the vault is installed as soon as the
- shared runtime base exists, and genesis delivers its secrets to it; the vault is the only maker.
-- [To-be 07](../03-DESIGN/01-to-be/07-the-foundation.md) is amended: genesis seals its values to
- the control-node's key as well as the operator key.
-- [To-be 12](../03-DESIGN/01-to-be/12-a-module-repository.md), [to-be 16](../03-DESIGN/01-to-be/16-module-coverage.md)
- and [to-be 18](../03-DESIGN/01-to-be/18-building-a-module.md) are amended: `own-secrets` is retired from the
- manifest they describe.
-- The [glossary](../00-META/glossary.md) gains *shared secret*, *recipient*, *applies* and *reads at
- start*, and *reserved provision*, once this record is accepted.
-- [Issue 103](../04-ISSUES/103-a-container-is-not-recreated-when-a-file-it-reads-changes/00-report.md)
- is a prerequisite, and its fix is in the host: a container is recreated when a file it read at
- creation changes. The issue is to be recorded as fixed, and derived restarts rest on it.
-
-## Consequences
-
-- **The vault is on the path of every new or rotated shared secret.** Today the controller holds that
- place, on the same node. A secret can no longer be made while the vault is down.
-- Resolution expands per-consumer requirements from a provision's contract. The contract declares
- them, never the provider's code, so what a provider requires stays predictable from the catalogue.
-- A data provider's adapter gains a return value. What a credential provider's adapter must change for
- rotation is [ADR 0114](0114-a-shared-credential-rotates-over-two-credentials.md)'s.
-- The broker's provisioner gains every bus account, and the controller loses five separate places it
- generates a secret today.
-- 54 modules move from own secrets to vault requirements. Six provider clients export a password
- generator nothing uses any more; it is removed, so no module can quietly start minting again.
-- The installation changes order: the vault is installed as soon as the shared runtime base exists,
- before any other module built on it.
-- **What got harder:** a secret some services read only at first start can no longer be "rotated" by
- a restart that quietly changes nothing; it is refused instead, or applied by its provisioner. And
- moving the vault or the broker is a procedure, not an assignment.
-
-## How it is checked
-
-| Rule | Checked by |
-|---|---|
-| Only the vault generates a shared secret after genesis | A controller test: no code path generates a shared secret. A catalogue test: no module's code generates one, found by scanning for generation calls. Exempt are the vault itself, and randomness that is not a secret any other party holds, such as a password hash's salt, each named in a declared list. An installer test: genesis generates exactly the list above and delivers it to the vault, recorded as the mesh's own. |
-| A private key is made where it is used | A test per key: a node's sealing key never leaves the node, the operator's private key never enters the mesh, and the certificate authority's private key never leaves the controller's identity store. |
-| Only the vault provides `secret` | The parser refuses a definition providing `secret` that cannot hold `mesh-vault`, and resolution refuses a pin on a `secret` requirement. |
-| The controller and each node's host take the same path | A catalogue test: the controller's definition declares no own secret, only requirements. A controller test: a node's bus account is made by the vault and delivered sealed to that node; an enrolment token reaches the controller only as what verifies it. |
-| Genesis's values reach the vault unattended, and the operator keeps a copy | An installer test: each of genesis's values is sealed to the control-node's key and to the operator key; the controller delivers the first to the vault when it is installed, with no operator step; the operator's copy opens only with the operator key. |
-| Moving the vault or broker is a procedure | A resolution test: an ordinary assignment moving `mesh-vault` or `mesh-broker` is refused, naming the procedure. |
-| A backend-issued secret enters through the vault | A vault test: a value delivered as issued is provided like any other, and rotating it is refused as the vault's act. |
-| A secret with no provisioner to apply it is not rotated by restart | A vault test: rotating a secret whose requirement is marked not rotatable by the mesh is refused, naming why. |
-| A rotation never destroys a consumer's data | A provider test per credential provider: rotating a consumer's credential leaves its resource and data intact. It fails today for no provider, because rotation is in place; it guards whichever mechanism replaces it. |
-| A provider's per-consumer secret comes from the vault | A resolution test: a consumer requiring a database expands to a secret requirement for it, answered by the vault and delivered to both recipients. |
-| Values are carried sealed | A controller test: each recipient's copy opens with that recipient's node key and no other; neither the controller nor a message on the broker can open one. |
-| Own secrets are retired | A catalogue test: no definition declares an own secret, with a declared list of exceptions that shrinks to empty. |
-| Bus accounts come from the broker's provisioner | A resolution test: assigning a module that speaks on the bus yields its account, created by the broker's provisioner with no separate command. |
-| An operator's value is never rotated by the vault, and genesis's values are | Vault tests: a rotation request on an operator's external key is refused, naming the operator; the same request on a value genesis delivered makes a replacement. |
-| Restarts are derived from how a secret is read | A host test: a secret read at start recreates the container that read it at creation, through an env-file or a direct mount; an applied secret restarts nothing. A catalogue test: a secret that reaches a process, or a file in a mounted directory, has `restart-on` naming it. |
-| A provider answers data back | A lab test with a consumer requiring analytics: the provider's site id reaches it as a resolved value. |
-
-## References
-
-- [ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md): the decision this supersedes,
- and the return path it left open
-- [ADR 0085](0085-a-secret-is-a-provision.md): the vault, and the objection this record answers
-- [ADR 0092](0092-an-operator-delivers-a-pair-credential.md), [ADR 0049](0049-a-consumers-identity-fits-the-tightest-backend.md),
- [ADR 0043](0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md): delivered values,
- identity, and broker accounts
-- [ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md), [to-be 27](../03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md):
- everything a module needs is a requirement
-- [Issue 095](../04-ISSUES/095-a-module-assigned-after-genesis-has-no-broker-account/00-report.md),
- [issue 103](../04-ISSUES/103-a-container-is-not-recreated-when-a-file-it-reads-changes/00-report.md): what fails today
-
-## Accepted, 2026-09-29, against what was built
-
-*Marked in a grooming pass.* The vault is a module providing `secret` at mesh scope, and six
-modules in the catalogue require it — so a shared secret is a requirement answered by the vault,
-which is what this record asks for. Private keys are still made where they are used and never
-travel, which is the other half and was never in question.
-
-
-> **The mechanism changed — 2026-10-02, by [ADR 0183](0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md).**
-> What stands: every shared secret the mesh makes is the vault's, a private key is made where it is
-> used, and a long-lived value a backend issues enters the vault's custody — here as the key the vault
-> makes for the licence manager, which encrypts the vendor's grants at rest with it. What this record
-> did not foresee: a credential that lives hours, issued by a vendor to the one module that holds its
-> grant, and handed by that module to the agent on each node sealed to that node's module key, on
-> request/reply over the bus, never through the vault and never as a file the host writes. ADR 0183
-> states that as a bounded exception — one vendor, tokens that live hours, one recipient per message —
-> and a second such channel is a decision of its own.
-
-> **The mechanism changed — 2026-10-06, by [ADR 0228](0228-a-value-given-by-hand-lives-only-until-its-modules-first-good-start.md).**
-> What stands: a delivered value the vault cannot replace, such as an external API key, is not rotated
-> by the vault, and rotating it means an operator delivering a new one. What moved: the controller had
-> read that as covering **every** value given to it, and refused to rotate any of them. 0228 says what
-> cannot be replaced is a value an outside party issues, which a module's definition now marks
-> (`"issued-by": "outside"`); a given value for a secret the module reads at start is rotated like a
-> made one, and one given through `secret accept` is replaced on its own after the module's first good
-> start under the mesh.
diff --git a/02-DECISIONS/0114-a-shared-credential-rotates-over-two-credentials.md b/02-DECISIONS/0114-a-shared-credential-rotates-over-two-credentials.md
deleted file mode 100644
index 19dee25c..00000000
--- a/02-DECISIONS/0114-a-shared-credential-rotates-over-two-credentials.md
+++ /dev/null
@@ -1,270 +0,0 @@
----
-topic: what runs on it
-status: accepted
-date: 2026-09-26
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0113-the-vault-makes-every-secret.md
----
-
-# 114. A credential two parties hold rotates over two credentials; one a single party holds rotates in place, staged; and retiring a credential never removes what it reached
-
-## Context
-
-[ADR 0113](0113-the-vault-makes-every-secret.md) decides who asks for a rotation (an operator, or the
-vault's policy) and who makes the new value (the vault). It leaves open how old and new change over.
-[Research 016](../01-RESEARCH/016-how-a-credential-can-be-rotated/00-overview.md) read every
-credential provider in the catalogue against its code. There are nine:
-
-- **all nine re-apply a password in place**, on the same login, every time they run. The controller's
- `rotate` command relies on that, and states the window it leaves: between the provider applying the
- new value and the consumer restarting with it, the consumer cannot authenticate;
-- **eight of nine name the consumer's resource after its login**: a database, a bucket, a virtual host,
- a key prefix, a topic prefix, a mailbox. Only the forge's npm registry keeps them apart, because an
- organisation owns the packages;
-- **five of nine destroy the consumer's data when they remove its login**: postgres, mssql and mongodb
- drop the database, lavinmq drops the virtual host with its queued messages, and mailu deletes the
- mailbox with its mail. In those adapters, *retire a login* and *delete the consumer's data* are one
- call. minio drops a bucket only if it is empty. The provisioner harness makes it worse: a consumer
- whose derived login changed is removed under the old login and created under the new one, in one pass;
-- **one backend holds two passwords on one login** (redis), and two hold several tokens beside one
- password (the forge and mailu);
-- **eight of nine can give two logins the same rights over one resource**. mailu cannot, because a mail
- user *is* its mailbox. It can give one user several tokens. In postgres, a second login is not enough
- on its own: objects belong to whichever login created them, so the resource must be owned by a role of
- its own;
-- **an administrative credential has one party and a fixed name.** The provider module both applies it
- and reads it. Five backends take it only at first initialisation: postgres, mssql, mongodb, mosquitto
- and lavinmq. Their credential file is mounted directly into both the server and the provisioner, so
- replacing the file recreates the provisioner holding only the new value, which the backend does not
- know yet. The provisioner is then locked out;
-- **no module watches a secret.** Every reader reads at start, and the host recreates a container when
- a file it read at creation changes
- ([issue 103](../04-ISSUES/103-a-container-is-not-recreated-when-a-file-it-reads-changes/00-report.md)).
-
-A first draft of 0113 chose to overlap old and new "through the adapter's existing create and
-remove". In five providers, that remove deletes the consumer's data. The mechanism has to be chosen on
-what the providers do, and the danger has to be closed whichever mechanism is chosen.
-
-## Considered Options
-
-**1. In place for everything, as today.** Works with every provider unchanged. Rejected for credentials
-two parties hold. The window cannot be closed, only shortened, and the two ends are on different
-machines with nothing ordering them. For an administrative credential, it locks the provisioner out.
-
-**2. Two secrets on one login.** Rejected as the mechanism. It works for three providers out of nine,
-and using it there and something else elsewhere would put the difference in the mesh instead of in the
-adapter.
-
-**3. Two logins over one resource.** Rejected as the mechanism. It works for eight of nine, and not for
-mailu.
-
-**4. Two credentials over one resource, with the adapter choosing what a credential is.** A credential
-is what a consumer presents, a login and a secret. The mesh alternates between two of them. Each adapter
-makes the second one the way its backend can: a second login for eight providers, a second token on the
-same login for mailu. A credential a single party holds is staged in place instead, and retiring a
-credential is separated from removing a consumer before either is used. Chosen.
-
-## Decision
-
-### Retiring a credential never removes what it reached
-
-**A provider's adapter keeps two things apart that today are one:** the consumer's *resource* (its
-database, bucket, virtual host, key or topic prefix, mailbox) and a *credential* that reaches it.
-They get separate operations:
-
-- **ensure the resource**, named after the consumer;
-- **ensure a credential** with a value, holding the consumer's rights over its resource;
-- **retire a credential**. Anything it owns moves first to the resource's owner, and any session it has
- open is ended. Then the credential is removed, and nothing else;
-- **remove the consumer**, which is what removes the resource, and retires every credential it has.
-
-**Remove the consumer runs only when the consumer no longer requires the provision from this provider.**
-That happens when its assignment goes, when its definition drops the requirement, or when re-resolution
-sends it to another provider. It never runs because a login or a value changed. The harness keys what it
-applied by the consumer, not by the login, so a changed login is a credential change and never a removal.
-What removing a resource does with the data in it stays
-[ADR 0030](0030-data-outlives-the-mesh-that-declared-it.md)'s, and re-resolving to another provider
-moves no data.
-
-**The resource is named after the consumer, and owned by the resource, not by a login.** A consumer's
-identity is derived from its assignment ([ADR 0049](0049-a-consumers-identity-fits-the-tightest-backend.md)),
-and today its login is that same string, so **no existing resource is renamed**. Where a backend makes
-whatever a login creates the login's own, as postgres does, the resource is owned by a role that cannot
-log in, and each credential works as that role. Ownership of an existing resource moves to it once. A
-credential is retired by handing what it owns to that role, never by dropping what it owns.
-
-### A credential two parties hold rotates over two credentials
-
-**Two parties** means an applier and a reader that are different modules, or a module and a node's
-host. The vault's custody copy does not count, because the vault holds every secret. So this covers a
-credential between a consumer and a provider, and every bus account: a module's or a host's, applied by
-the broker's provisioner and read by its owner. **Each consumer has two credentials, one in use at a
-time**, both holding the same rights over the one resource. For eight providers the second is a second
-login, derived by the mesh as the consumer's identity with a short fixed suffix. For mailu it is a
-second token on the same login.
-
-**The vault drives each rotation and records every step durably.** A provisioner learns which
-credentials to hold from what it receives: both of them, for as long as a rotation is under way. It
-never learns them from its own memory, so a provisioner restarted mid-rotation resumes from the step the
-vault has recorded.
-
-1. **The vault makes the new value.**
-2. **Each applier ensures the unused credential with it**, with the consumer's rights, and leaves the
- one in use untouched. It verifies that the new credential authenticates and the old one still does,
- and confirms. It repeats the confirmation on every reconcile pass until the vault acknowledges it, so
- a lost message costs one pass.
-3. **Only then does the vault release the new credential to the readers.** The mesh delivers it and the
- value together, and the host recreates each reader, because a file it read at creation changed. A
- node's host is its own reader: it reconnects to the bus with the new login, and confirms over it.
-4. **Each reader confirms by authenticating with the new credential.** It shows this through its
- health check, where its definition declares one, or the applier sees the new credential in use,
- where its backend reports that. A reader for which neither is possible is confirmed by an operator.
- It is never assumed from the reader having restarted.
-5. **Only when every reader has confirmed is the old credential retired**, as above, and verified to no
- longer authenticate.
-
-**A reader that goes away leaves the rotation.** A reader unassigned, or re-resolved to another
-provider, is no longer waited for. A consumer removed mid-rotation has both of its credentials retired
-with it.
-
-**A rotation can be abandoned until the old credential is retired.** An operator abandons it. Readers
-that moved are given the old credential back, and recreated. The new credential is retired. Nothing is
-lost, because the old one was never removed.
-
-`status` shows a rotation as waiting on whichever applier or reader has not moved, and it is not done
-until the old credential is gone. A reader that cannot be reached keeps working on the old credential
-until it can, and the rotation waits for it. That wait is shown, never hidden.
-
-**Queues and permissions belong to the consumer, not to a login.** A module's queue on the bus is named
-for the module on its node, and both of its logins get the same permissions over it
-([ADR 0043](0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md)). An MQTT client
-identifier is chosen by the consumer and is independent of its login. A reader recreated with a new
-login keeps it, and the broker hands the session over.
-
-### A credential a single party holds rotates in place, staged
-
-This covers a provider's administrative credential and a module's own secret, which only that module
-reads. The vault makes the new value, and the one party takes it:
-
-- **applied**: the vault delivers the new value **staged, beside the current one**, and the current file
- is left as it is. The party's provisioner changes the backend using the current value, verifies the
- new one, and confirms. Only then does the vault make the new value current. This is the only form for
- a backend that takes its administrative credential only at first initialisation. Replacing the file
- first would lock the provisioner out;
-- **read at start**: the vault delivers the new value as current, and the host recreates the party.
-
-There is no window between two parties, because there is only one. Where neither form can change the
-value, the requirement is marked not rotatable by the mesh, and a rotation is refused, saying why
-([ADR 0113](0113-the-vault-makes-every-secret.md)).
-
-### One rule decides which
-
-**The number of parties decides, never the provider.** The resolver knows it from the requirement's
-recipients, leaving out the vault's custody copy, so no definition declares it.
-
-> **Progressive insight — 2026-10-01.** The number of parties is the resolver's to know; *which form*
-> a single party's credential takes is not, and cannot be: whether a module reads its secret when it
-> starts or applies it once to a backend is a fact about the software, visible nowhere in the graph.
-> So the definition declares that half — `taken: at-start` or `taken: applied` on an own secret — and
-> a secret that declares neither is not rotated, refused with the word to write (issue 180). The
-> read-at-start form is built; the staged form for an applied credential is not. The decision stands;
-> the sentence above was one fact short.
-
-### Until an adapter can
-
-**An adapter that cannot yet ensure a second credential says so.** The two-party credentials it applies
-rotate in place, as today, and the window is stated when the rotation is asked for. So does a
-two-party credential whose backend has one fixed name and no second credential for it. These are listed
-by a check, and the list is meant to shrink. Separating *retire a credential* from *remove the
-consumer*, and keying the harness by consumer, come first. They close a data-loss path that exists
-today, whatever rotation does.
-
-## What this changes in earlier records
-
-On acceptance, each of these is amended by this record, not edited:
-
-- [ADR 0113](0113-the-vault-makes-every-secret.md): the changeover it left open is decided here.
-- [ADR 0049](0049-a-consumers-identity-fits-the-tightest-backend.md): a consumer's identity leaves room
- for the second login's suffix within the tightest backend it reaches, and both logins are checked
- against it.
-- [ADR 0043](0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md): a module's broker
- account is two logins with the same permissions over the same queue, one in use at a time. Its scoping
- is unchanged.
-- [ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md), already superseded by 0113:
- a provider now ensures and retires credentials over a resource it owns separately.
-- [To-be 13](../03-DESIGN/01-to-be/13-credentials-and-their-rotation.md): rotation of a two-party
- credential is no longer all-or-nothing with a window. It overlaps, with each step confirmed.
- A single-party credential is staged, not replaced.
-
-## Accepted, 2026-09-30, and not scheduled
-
-Accepted as written. The separation it draws — a consumer's *resource* and a *credential that reaches
-it* are different things with different lifecycles — is the part that had to be settled, because the
-alternative is what the record was written against: retiring a credential taking the data it reached
-with it. That is a data-loss shape, and a record that names it should not sit unresolved while the
-code that could hit it is being written.
-
-**It is not built, and accepting it does not schedule it.** The SDK's provisioner adapter is still
-`create` / `remove` / `holds` rather than the four operations above, and no provider implements the
-two-credential rotation. Accepted-and-not-built is an ordinary state here — 0141 and 0142 are both in
-it — and it is the honest one: leaving this `proposed` made it invisible to anyone reading what the
-mesh has decided, while changing nothing about what runs.
-
-The work it implies belongs with the provisioner contract, beside
-[issue 124](../04-ISSUES/124-a-consumer-cannot-be-told-what-its-provider-derived/00-report.md).
-
-## Consequences
-
-- **Every credential provider's adapter changes**, in two steps. The first separates *retire a
- credential* from *remove the consumer*. It names and owns the resource after the consumer, which
- keeps the name it has but moves ownership once in postgres and mssql, and the harness is keyed by
- consumer. The second ensures a second credential with the same rights.
-- **The vault gains rotation state**: each rotation's step, per applier and reader, recorded durably.
- Staged delivery is added for single-party secrets. The SDK harness carries the alternation and the
- repeated confirmation, so no adapter implements them.
-- **No consumer module changes.** It reads one credential at start, as today, and is recreated by the
- host when it changes. The exception is a reader that has neither a health check nor a backend that
- reports use: its rotations wait for an operator until it declares one.
-- **The derived identity is two characters tighter** in the tightest backend, a minio access key of 20
- characters.
-- **What got harder:**
- - a provider briefly holds two credentials per consumer;
- - a rotation lasts until its slowest reader moves, so an unreachable reader keeps the old credential
- valid until it is reached;
- - an adapter has four operations where it had two;
- - retiring a login in mssql has to end its sessions first.
-
-## How it is checked
-
-| Rule | Checked by |
-|---|---|
-| Retiring a credential never removes a resource | A provider test per credential provider: retiring one of a consumer's credentials leaves its resource and data intact, reachable through the other. |
-| What a retired login owned survives it | A postgres and an mssql test: objects created under login A, tables included, are still there and alterable under login B after A is retired. |
-| A changed login is not a removal | A harness test: changing a consumer's derived login ensures a credential and never calls remove. |
-| Remove runs only when the requirement goes | Harness tests: unassigning, dropping the requirement and re-resolving each remove the consumer once; a rotation and a login change never do. |
-| No existing resource is renamed | A provider test: a consumer created before the change keeps its resource, with ownership moved to the resource's own role where the backend needs one. |
-| Both credentials hold the same rights | A provider test per credential provider: data and structure created under one credential are read, changed and altered under the other. |
-| Readers move only after the applier confirms | A rotation test: readers receive nothing until both credentials authenticate at every applier. |
-| A reader confirms by authenticating | A rotation test: a reader recreated but failing to authenticate with the new credential does not confirm, and the old credential is not retired. |
-| The old credential is retired only after every reader confirms | A rotation test with one reader's node unreachable: it keeps authenticating with the old credential, the rotation shows waiting on it, and it completes when the reader returns and confirms. |
-| A reader that goes away leaves the rotation | A rotation test: unassigning a waiting reader lets the rotation complete; removing the consumer mid-rotation retires both credentials. |
-| A rotation can be abandoned | A rotation test: abandoning after readers moved gives them the old credential back and retires the new one. |
-| Rotation state survives a restart | A test restarting the applier's provisioner, and then the vault, between steps: the rotation resumes from the recorded step. |
-| A single-party applied secret is staged | A rotation test on a first-initialisation administrative credential: the provisioner receives the new value beside the current one, applies it, and only then does the new value become current. At no point does it lose its connection. |
-| The number of parties decides | A resolution test: a secret with an applier and a reader in different parties is marked for two credentials, and one held by one module for in place. The vault's copy is not counted. |
-| Both logins fit the tightest backend | A controller test: both derived logins for the longest node and module names fit the limit ADR 0049 sets. |
-| A host rotates its bus login | A rotation test on a node's bus account: the host reconnects with the new login and confirms over the bus before the old one is retired. |
-| What still rotates in place is listed | A catalogue test lists every adapter that cannot yet ensure a second credential, and every two-party credential with one fixed name. A rotation of these states its window. |
-
-## References
-
-- [Research 016](../01-RESEARCH/016-how-a-credential-can-be-rotated/00-overview.md): the survey this
- rests on, provider by provider
-- [ADR 0113](0113-the-vault-makes-every-secret.md): who asks and who makes
-- [ADR 0049](0049-a-consumers-identity-fits-the-tightest-backend.md), [ADR 0043](0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md),
- [ADR 0030](0030-data-outlives-the-mesh-that-declared-it.md): identity, bus accounts, and data outliving
- its declaration
-- [To-be 13](../03-DESIGN/01-to-be/13-credentials-and-their-rotation.md): rotation as implemented
-- [Issue 103](../04-ISSUES/103-a-container-is-not-recreated-when-a-file-it-reads-changes/00-report.md):
- why a reader's restart can be derived
diff --git a/02-DECISIONS/0115-one-assignment-of-a-module-per-node.md b/02-DECISIONS/0115-one-assignment-of-a-module-per-node.md
deleted file mode 100644
index 5bfc9f85..00000000
--- a/02-DECISIONS/0115-one-assignment-of-a-module-per-node.md
+++ /dev/null
@@ -1,56 +0,0 @@
----
-topic: what runs on it
-status: accepted
-date: 2026-09-26
-deciders: jochen
-extends: 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
----
-
-# 115. One assignment of a module per node: the module's name is the assignment's identity
-
-## Context
-
-Everything an assignment owns is named after its module: the database user
-(`mesh__`), the broker account (`-`), the containers, the sealed
-secrets, and — since ADR 0112 — the placed directory (`/`). A second assignment
-of the same module on the same node would collide on every one of those names at once, which
-is why the mesh has never allowed it.
-
-[Issue 119](../04-ISSUES/119-a-module-definition-decides-where-its-files-live/00-report.md)
-recorded this as a kept limitation, and 0112 deliberately did not fix it — giving assignments
-identities of their own would have touched every naming recipe in one already-large change.
-The question stayed open: is multi-assignment a requirement deferred, or a requirement at all?
-
-The original motivation was real — one module serving two tenants on one machine, a second
-photo site, a second mail domain. The operator held that requirement once and has now weighed
-it against what it costs.
-
-## Decision
-
-**Dropped.** One assignment of a module per node is the rule, not a limitation. The module's
-name IS the assignment's identity on a node, permanently, and every naming recipe may rely on
-it.
-
-Wanting the same software twice on one node has a spelling the mesh already supports: **two
-modules.** A module definition is cheap — two photo sites are two modules sharing artifacts
-(the build's images are content-addressed; nothing is built twice), each with its own name,
-its own directory, its own grants and its own routes. The tenant boundary lands where every
-other boundary already is: the module name.
-
-## Consequences
-
-- The naming recipes stay as simple as they are. No instance suffixes, no assignment ids
- threaded through six systems, no migration of every existing name.
-- `/` is the assignment's directory with nothing left open (0112's placement
- language stands unchanged).
-- The controller may refuse a second assignment *plainly* — "novox already runs mailu, and one
- node runs one of each (ADR 0115)" — instead of failing on whichever name collides first.
-- Multi-tenant asks are answered in the catalogue (a second module definition), not in the
- control plane.
-
-## Accepted, 2026-09-29, against what was built
-
-*Marked in a grooming pass.* The rule is enforced where it cannot be forgotten: `assignment`'s
-primary key is `(node, module)`, so a second assignment of one module to one machine is not a thing
-the mesh can hold. The record read `proposed` while the schema had already settled it.
-
diff --git a/02-DECISIONS/0116-the-bus-is-built-in-five-steps.md b/02-DECISIONS/0116-the-bus-is-built-in-five-steps.md
deleted file mode 100644
index 19a7319b..00000000
--- a/02-DECISIONS/0116-the-bus-is-built-in-five-steps.md
+++ /dev/null
@@ -1,175 +0,0 @@
----
-topic: the mesh
-status: accepted
-date: 2026-09-26
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0106-the-bus-is-nats.md
----
-
-# 116. The bus is built in five steps, and the protocol moves with it
-
-## Context
-
-[ADR 0106](0106-the-bus-is-nats.md) decided the bus is NATS and described the change as one thing:
-built beside the migration, cut over in one rollout after its core. The architecture was written as
-[design 25](../03-DESIGN/01-to-be/25-the-bus-on-nats.md) and revised once after review. What neither
-says is how the work is divided, and three gaps follow from that.
-
-**The whole of the build is one point.** Design 25 §9 numbers four items. The first — "the `nats`
-module, the controller's and host's link on NATS, the runtime's client — built and proven in the
-lab" — is every line of code the change requires; the other three are the rollout. A step of that
-size ends at nothing provable until it ends at everything, which is the failure
-[design 22](../03-DESIGN/01-to-be/22-the-work-ahead.md) opens by naming: *a phase that ends at a
-claim is a phase that went missing without anything complaining.*
-
-**There is no adoption path.** Design 25 §5 says the broker "is raised at genesis like the store,
-adopted as a module in the same phase" — which describes a mesh being raised from nothing. The mesh
-this is for is already running, and a running node gets a foundation module by adoption in place
-([ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md)), not by genesis. §9 goes
-straight from "built beside" to "one rollout" and never crosses that gap.
-
-**The wire changes and the protocol specification does not know it.** Design 25 §8 says a module
-sees nothing new. That is true of the SDK's contract — `request`, `handle`, `publish`, `subscribe`
-— and false of the wire underneath it.
-[ADR 0074](0074-the-wire-is-specified-not-the-types.md) settled that what an SDK implements is a
-*specified wire*, checked by fixtures that must match byte for byte, precisely because two
-implementations that disagree about an envelope do not fail to compile. That specification is
-[design 19](../03-DESIGN/01-to-be/19-the-module-protocol.md), and it is written entirely in AMQP:
-exchanges, a durable per-consumer queue named `..events`, a shared `serve.`
-queue — sixteen occurrences of an AMQP term across the document. Design 25 does not cite design 19
-anywhere, and design 19 does not cite ADR 0106. So the record that says *what two implementations
-may not disagree about* still describes the bus being replaced.
-
-## Considered Options
-
-1. **Keep ADR 0106's shape — build it all, cut over once.** Rejected: not for its rollout, which
- is right, but because it leaves the build a single step of unknown length with no intermediate
- anyone can run. The three gaps above were found by dividing it; they were invisible while it
- was one item.
-2. **Cut over incrementally by traffic kind** — events to NATS first, then tools, then control,
- the mesh on two buses meanwhile. Rejected: ADR 0106 already rejected two buses, and this is
- that with extra steps. The store-window guarantee
- ([ADR 0083](0083-one-push-leaves-the-mesh-consistent.md)) is exactly the one that cannot cross
- a seam, and control is exactly the traffic that carries it.
-3. **Five steps, each ending at something provable, the cutover still one rollout.** The build is
- divided; the bus still moves once. Adopted.
-
-## Decision
-
-**The bus is built in five steps. Each ends at something a lab bed proves, and no step's proof
-waits for the one after it. The cutover remains a single rollout** — dividing the build does not
-divide the bus.
-
-**Step 1 — genesis raises the broker.** The `nats` module, and genesis placing it in the
-foundation. This is built and proven even though the mesh it is for will never travel this path,
-because
-genesis is where the foundation is *defined*: the only place the mesh comes from nothing, and the
-definition every other path is measured against. A genesis path that exists only on paper is one
-nobody discovers is wrong until there is a second mesh.
-
-**Step 2 — adoption puts the broker in the seat.** A mesh already running receives the broker by
-adoption in place, and the seat it claims is **`mesh-broker`** — unchanged.
-[ADR 0079](0079-the-foundation-seats-are-named-after-their-servers.md) named the foundation seats
-after the server's *role* rather than the product for exactly this case, and ADR 0106 restated it:
-the broker module changes, the seat does not. A seat named after the product would have to be
-renamed by every change the seat exists to survive.
-
-**Step 3 — the protocol gets a NATS binding.** ADR 0074 stands unamended: the mesh defines a module
-protocol, an SDK is an implementation of it in one language and nothing more, the protocol is split
-per capability, and conformance is executable fixtures per capability rather than prose. What
-changes is what the specification specifies. Design 19's wire section is rewritten from exchanges
-and queues to subjects and streams; the conformance suite is built — first against the bus the
-mesh has, then restated on NATS; every SDK claims the capabilities it passes, and a language may
-arrive with connection and events alone.
-
-**The SDK gains no conveniences in the process.** [ADR 0039](0039-what-the-sdk-holds-and-refuses.md)
-refuses frequent-and-cascading code in the shared library, and ADR 0074 restates it — an SDK is
-"not a convenience layer, not a place for helpers to accumulate." A new transport is the moment
-that pressure is highest and the reason to hold hardest: the predecessor's shared library is the
-cautionary tale ADR 0039 opens with, and it did not become that in one decision. Code shared among
-a module's own features stays in that module.
-
-**Step 4 — the core speaks NATS.** The controller's link, the host's link and the tool runtime's
-client, and with them the flows that are today carried by something other than the bus: a build
-source's change reaching the builder, an installation, a module's own reports. Each is a
-conversion with a named before and after, not a rewrite. Observation — heartbeats, conditions,
-key-value state — is [research 017](../01-RESEARCH/017-a-mesh-that-heals-itself/00-overview.md)'s
-and stays there; that effort already reserves it for after the move, and this step does not
-pre-empt its design.
-
-**Step 5 — deployment.** The rollout ADR 0106 decided, unchanged: the controller, every host and
-every runtime move together, each node confirmed to have heard before AMQP stops. What this step
-adds is that steps 1 to 4 *ship ahead of it* without moving any node's bus — the module exists in
-the catalogue, the protocol is specified, the code is written and beds pass, and the running mesh
-is still on AMQP throughout. The bus moves on one day, at the end, once.
-
-## Consequences
-
-- **Design 25 §9 is replaced by these five steps** and §10's beds are attributed to the step each
- proves, so no proof waits for the last step.
-- **Design 19 is stale in its wire section from today** and says so in its own frontmatter and
- opening until step 3 rewrites it. A specification that describes the bus being replaced is worse
- than an absent one, because it reads as current.
-- **`nats-broker` is not a seat.** The module is `nats`; the seat is `mesh-broker`.
-- **Steps 1 to 4 leave every node on AMQP.** A step can be abandoned, or reordered after step 2,
- without a rollback — the cost of being wrong is bounded until step 5.
-- **What got harder:** five steps mean five proofs rather than one, and step 3 rewrites a design
- other designs cite, so their references are checked when it lands. Dividing the work does not
- reduce it.
-
-## How it is checked
-
-- **Per step, a bed, and the bed named in design 25 §10 against the step it belongs to.** Step 1:
- a mesh raised from nothing has the server standing, its streams asserted and every account
- composed from the manifests — no mesh traffic on it yet. Step 2: the broker adopted into a mesh
- already running, a second holder of `mesh-broker` refused at resolution, and nothing routed to it.
- Step 3: the conformance suite passing per capability, on NATS, for every implementation that
- claims it — and a module built before the binding serving its tools unchanged. Step 4: each
- converted flow proved against the behaviour it replaced, **and the full genesis bed** — a mesh
- enrolling, holding a push, and rolling out an upgrade on NATS. Step 5: the cutover bed, then the
- rollout with every node reporting.
-- **Step 3 is not done when the code runs.** It is done when the fixtures match byte for byte
- across implementations, which is ADR 0074's own test and the only one that catches two SDKs
- quietly ignoring each other.
-- **A step that cannot name what its bed proves is not a step**, and is divided further before it
- is started.
-
-## Progressive insights
-
-Corrections of fact made in this record after it was accepted, under the rule in
-[`README.md`](README.md). The decision — five steps, each proved, one rollout — is untouched by
-both; each corrects something this record asserted about the *state of the code*, which nobody had
-measured when it was written.
-
-> **Progressive insight — 2026-09-26.** *There is no conformance suite to recapture.* This record
-> said step 3's "fixtures are recaptured on NATS", and design 19's wire was described as specified
-> and conformed. Measuring the four repositories found no conformance fixtures in any of them:
-> [design 22](../03-DESIGN/01-to-be/22-the-work-ahead.md)'s Phase 1.2, which would have built the
-> suite, is still open. Step 3 therefore **builds** it, and builds it first against the bus the
-> mesh has — a suite born on the new bus certifies whatever the new bus happens to do. The step's
-> place in the order, and ADR 0074's model it implements, are unchanged.
-
-> **Progressive insight — 2026-09-26.** *The full genesis bed belongs to step 4, not step 1.* This
-> record attributed "a mesh raised on NATS from genesis" to step 1, and described that step as "the
-> `nats` module and a mesh raised on it from nothing". A bed that enrols a node, holds a push and
-> rolls out an upgrade needs the controller and the host to speak NATS — which is step 3's
-> implementations and step 4's flows. A step whose proof cannot run is exactly the failure this
-> record was written to prevent, so step 1 now ends at the server standing from genesis, correctly
-> configured and carrying nothing, and the full bed is named under step 4. The five steps, their
-> names, their order and the single rollout are unchanged; only where two beds run has moved.
->
-> Recorded here rather than superseded because the decision this record makes — that the work is
-> divided and each division is proved — is what *produced* the correction: the dependency was
-> invisible while the build was one item.
-
-## References
-
-- [ADR 0106](0106-the-bus-is-nats.md) — the decision this divides.
-- [ADR 0074](0074-the-wire-is-specified-not-the-types.md) — the protocol and its conformance suite;
- step 3 is its NATS binding, not a replacement.
-- [ADR 0039](0039-what-the-sdk-holds-and-refuses.md) — why step 3 adds no helpers.
-- [ADR 0079](0079-the-foundation-seats-are-named-after-their-servers.md) — why the seat is
- `mesh-broker`.
-- [ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md) — the adoption step 2 uses.
-- [design 25](../03-DESIGN/01-to-be/25-the-bus-on-nats.md), [design 19](../03-DESIGN/01-to-be/19-the-module-protocol.md) — the two documents this changes.
diff --git a/02-DECISIONS/0117-a-machines-uplink-is-a-seat.md b/02-DECISIONS/0117-a-machines-uplink-is-a-seat.md
deleted file mode 100644
index b9756bea..00000000
--- a/02-DECISIONS/0117-a-machines-uplink-is-a-seat.md
+++ /dev/null
@@ -1,139 +0,0 @@
----
-topic: what runs on it
-status: accepted
-date: 2026-09-26
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md
----
-
-# 117. A machine's uplink is a seat: the mesh configures the manager, never the link
-
-> **The mechanism changed — 2026-10-06, by [ADR 0226](0226-the-private-network-is-assigned-by-its-own-name-and-the-proxy-names-its-public-issuer.md).** The seat stands; the catalogue holds two
-> of its three modules. `dhcpcd`, held by no machine, left it. What this record says of dhcpcd is what
-> a module for it must do, if one is written again.
-
-## Context
-
-The mesh installs on top of a machine's own networking. The private network's generator says
-so in as many words: a machine has an address and a route to the broker *before* the mesh
-exists, the broker's address travels in the enrolment token rather than being resolved, and the
-private network is something the mesh installs on top, like anything else. Nothing in the mesh
-says who manages that uplink, or what the mesh needs from whoever does.
-
-Adopting the first workstations showed that the mesh does need something from it, and gets it
-by accident:
-
-- **The resolver the mesh owns depends on a file the mesh does not.** `resolv-conf` writes
- `/etc/resolv.conf` and names the mesh's resolver. On a machine running NetworkManager, the
- manager rewrites that file on every connectivity change unless it is told `dns=none`; on one
- running dhcpcd, every lease renewal rewrites it unless it is told `nohook resolv.conf`. On
- the adopted machines both settings exist only because the predecessor wrote them. No module
- declares them. Remove the predecessor's file and the mesh's resolver is silently replaced the
- next time a laptop changes network, while every surface of the mesh still reads green.
-- **`resolv-conf` cannot declare them itself.** Which setting is needed depends on which
- manager runs, and a `service` resource for a manager that is not installed fails the
- declaration. A resolver module that knew about network managers would be the wrong module
- knowing the wrong thing.
-- **The private network's interface is exposed to the manager.** A manager that considers
- every interface its own may try to configure `mesh0`, or tear it down on a profile change.
- Nothing tells it not to.
-- **Two managers on one machine go unnoticed.** Among the machines adopted so far, one runs
- NetworkManager *and* dhcpcd at once: two programs that each believe they own the machine's
- addresses and its resolver file.
- Nothing detected it, because nothing in the mesh knows the role exists.
-
-The machines differ in a way that matters: servers are wired and never move, while
-workstations join wireless networks, captive portals and phone hotspots wherever they are.
-
-## Considered Options
-
-**1. The mesh manages the uplink: links, addressing, wireless networks and their
-credentials.** Rejected. The mesh reaches a machine only over that link. A declaration that
-gets it wrong — a mistyped network, a stale credential, a manager that fails to start — takes
-the machine off the network, and with it the only channel a fix could arrive on. That is the
-one failure the sshd module's `listens` rule forbids the firewall to arrange; a mesh that owned
-the link could arrange it with any push. And a wireless network is joined at the machine, by
-the person using it, in the moment. A declaration composed elsewhere cannot answer a captive
-portal.
-
-**2. Leave the uplink unmanaged; accept the implicit dependency.** Rejected. It keeps the
-resolver working only for as long as a predecessor's file survives, and it leaves two managers
-on one machine undetectable.
-
-**3. The uplink is a seat. The module holding it configures the manager's relationship to the
-mesh, and never the link.** Chosen.
-
-## Decision
-
-**`the-uplink` is a node-scoped seat** in the closed set ([ADR 0110](0110-a-seat-is-a-module-assignment-from-a-closed-set.md)).
-It delivers no provision. It is held by the module for the program that manages the machine's
-own network, one per manager: `networkmanager`, `systemd-networkd`, and `dhcpcd` for a machine
-with nothing more. Assigning a second is refused, naming the first.
-
-**What a holder declares** — only what keeps the manager and the mesh from contradicting each
-other:
-
-- the manager's package, present — and its service **with no state**: the manager's lifecycle is
- the machine's. The mesh never starts, stops, enables or disables it, because stopping it takes
- the link down, and a holder unassigned by mistake — or the wrong holder assigned — must not be
- able to do that, nor start a second manager beside the one the machine runs. The service is
- declared only so a change to the holder's settings reaches a *running* manager;
-- the manager's own configuration that leaves the resolver file to the mesh (`dns=none` for
- NetworkManager, `nohook resolv.conf` for dhcpcd, and nothing for systemd-networkd, which
- never writes the resolver file);
-- the manager's own configuration that leaves the private network's interface alone
- (NetworkManager's `unmanaged-devices` naming `mesh0`; dhcpcd's `denyinterfaces mesh0`; for
- systemd-networkd a network file of the module's matching `mesh0` as `Unmanaged=yes`);
-- each as a drop-in beside the manager's main file where the manager reads one, and written
- *into* a shared file otherwise, as a marked region the host owns
- ([ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md)'s idea for text files),
- placed where the manager reads it as global — at the start of `dhcpcd.conf`, above any
- `interface` line, because every line after one belongs to that interface;
-- the service **reloaded** when a drop-in changes, never restarted — a restart drops the link,
- and the link is the mesh's own channel to the machine. **A manager that cannot reload is not
- restarted instead:** its setting takes effect at the manager's next start. Measured on the
- adopted machines: NetworkManager (1.58) and systemd-networkd (systemd 261) both report
- `CanReload=yes`; dhcpcd (10.3) reports `CanReload=no`, so its module declares no trigger at
- all. Whether each setting is actually *applied* by a reload is confirmed on a machine before
- the module is taken there, not assumed.
-
-**What a holder never declares:** a link, an address, a route, a connection profile, a
-wireless network or its credentials. Those are the operator's, in the sense of
-[ADR 0051](0051-shared-data-is-the-operators.md): the mesh does not create, change or delete
-them, and the module's `access`, if it needs one, is read-only.
-
-## Consequences
-
-- `resolv-conf` stays generic. The condition it could not express — "only if NetworkManager
- runs" — is expressed by assigning the module for the manager that does.
-- The dependency on the predecessor's `dns=none` file becomes a declared resource. On an
- adopted node the holder's drop-in arrives beside the predecessor's; both say the same thing,
- and the predecessor's is retired by hand after the take, like any other file the mesh
- replaced under another name.
-- A setting a manager reads only at its start is not in force until then. On an adopted machine
- the predecessor's identical line normally already is; on a machine that was not adopted,
- dhcpcd's resolver hook keeps rewriting the resolver file until dhcpcd next starts, and the
- operator restarts it once, in a window of their choosing.
-- A machine running two managers is found at assignment: the second holder is refused, and the
- operator decides which manager the machine keeps before either module is taken.
-- Workstations keep joining networks the way they always have. Under NetworkManager and
- systemd-networkd the host already cooperates with the manager — its dispatcher hook wakes it
- on every connectivity change — and nothing here changes that. A dhcpcd-only machine has no
- such hook, and nothing here adds one.
-- The seat table gains one entry: `the-uplink`, node scope, delivering nothing, decided here.
-- **Not decided here:** whether the mesh should ever *offer* known networks to a machine — a
- sealed, add-only list the operator curates once for all workstations. That is a different
- question (the mesh holding credentials for links it must never be able to break) and gets its
- own record if it is wanted.
-
-## References
-
-- [ADR 0110](0110-a-seat-is-a-module-assignment-from-a-closed-set.md): the closed set this seat joins;
- [to-be 26](../03-DESIGN/01-to-be/26-the-seats.md): the seat table
-- [ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md): written into, never over
-- [ADR 0051](0051-shared-data-is-the-operators.md): what is the operator's stays the operator's
-- mesh-controller `internal/catalogue/seats.go` (the seat), `internal/overlay/generator.go` (the
- mesh installs on top of the machine's own networking)
-- mesh-catalog `modules/networkmanager`, `modules/systemd-networkd`, `modules/dhcpcd`
-- mesh-host `internal/apply/block.go` (a file written into a marked region, `at` start or end)
diff --git a/02-DECISIONS/0118-undeclaring-gives-a-unit-back-the-state-it-was-found-in.md b/02-DECISIONS/0118-undeclaring-gives-a-unit-back-the-state-it-was-found-in.md
deleted file mode 100644
index 7947cbdb..00000000
--- a/02-DECISIONS/0118-undeclaring-gives-a-unit-back-the-state-it-was-found-in.md
+++ /dev/null
@@ -1,125 +0,0 @@
----
-topic: what runs on it
-status: accepted
-date: 2026-09-27
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0102-the-mesh-writes-into-a-shared-file-never-over-it.md
----
-
-# 118. Undeclaring removes what the mesh made, and gives a unit back the state it was found in
-
-## Context
-
-When a resource stops being declared — its module unassigned, the node sent a
-deliberately-empty declaration ([issue 149](../04-ISSUES/149-a-declaration-that-shrinks-to-empty-is-skipped-not-sent/00-report.md)),
-or a new catalogue version renaming its id — the host undoes it. The host's own code states
-the rule it means to follow: **it removes what it made and leaves what it merely configured.**
-For almost every resource it does exactly that:
-
-- a container, a network, a process's unit, a directory it created: removed;
-- a file it created: removed; a file it replaced: its kept original put back
- ([ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md));
-- keys and list members it wrote into a shared file: given back as they were
- ([ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md));
-- a package: left installed — the host cannot know it is unused;
-- an operator's path it was given access to: never touched
- ([ADR 0051](0051-shared-data-is-the-operators.md)).
-
-**A service is the exception.** A `service` resource never installs a unit: it puts one that
-already exists — the distribution's, the operator's — into a state. Undeclared, the host stops
-it. That contradicts the rule above, and in practice it is the most dangerous thing an
-undeclare can do. Found reviewing the uplink modules
-([issue 130](../04-ISSUES/130-undeclaring-a-service-stops-it/00-report.md)):
-
-- the private network declares the container runtime's unit only so a change to the registry
- trust reloads it — unassigning the private network stops the runtime, and every container on
- the machine, the mesh's and not;
-- the sshd module declares the ssh daemon — unassigning it stops ssh, the lockout that module's
- own `listens` rule forbids;
-- the uplink modules would have stopped the network manager, taking the machine off the only
- link the mesh reaches it by.
-
-[ADR 0125](0117-a-machines-uplink-is-a-seat.md) answered that for its own modules with a
-service declared with no `state`. Every other module that declares a unit it did not make is
-exposed in the same way, and relying on each author to remember an opt-out is how the next one
-is missed.
-
-## Considered Options
-
-**1. Undeclaring touches nothing on the machine.** Rejected. What the mesh made would outlive
-the module that made it: a container nobody manages keeps serving and stops being patched; a
-unit the mesh wrote keeps running a bundle nothing updates; a name collides when the module
-is assigned again. An undeclare that leaves the mesh's own work behind is an orphan factory.
-
-**2. Keep stopping services; make "leave it running" an opt-in per resource.** Rejected. It
-keeps the dangerous behaviour as the default for exactly the units that matter most — the
-runtime, the ssh daemon, the network — and each new module is one forgotten field away from a
-machine that goes dark when it is unassigned.
-
-**3. Never stop a unit the mesh did not create.** Rejected, found while implementing it. The
-mesh's packet filter is a unit the distribution installed and the mesh started at converge;
-returning a node to adopted ([ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md))
-unloads it by undeclaring it. Never stopping it would leave the mesh's filter loaded beside the
-predecessor's firewall re-enabled — the one rollback a converge promises, broken. Who wrote the
-unit file is not the line; what the mesh *did* to the unit is.
-
-**4. Give the unit back the state it was found in.** Chosen.
-
-## Decision
-
-**Undeclaring removes what the mesh made and gives back what it changed.** For a unit the mesh
-did not create, what it changed is the unit's state, so that is what is given back: **the host
-records the state it first found the unit in, and undeclaring returns the unit to it.**
-
-- **Recorded once**, the first time the host applies the service — whether it was running, and,
- where the declaration sets it, whether it was enabled at boot — and carried in the host's
- record from then on. Later applies never overwrite it: by then the unit's state is the mesh's
- doing.
-- **A unit found running is left running.** The container runtime, the ssh daemon, a network
- manager: running before the mesh arrived, running after it leaves.
-- **A unit the mesh started is stopped again**, and one it enabled is disabled again — the packet
- filter a converge loaded, which returning to adopted unloads.
-> **Superseded in part — 2026-10-09, by [ADR 0269](0269-what-the-meshs-own-declaration-stopped-is-started-again-when-it-goes.md).**
-> "Never started on the way out" has one exception. The host now records what the mesh declared beside
-> what it found. A unit found running or enabled that the mesh's own declaration held stopped or disabled
-> is started or enabled again when that declaration goes, but only while it is still exactly as the mesh
-> held it: a masked unit, or one changed since, is left as it is. The rest of this record stands.
-
-- **Never started on the way out.** A unit the mesh stopped is not started again when its
- declaration goes; starting something is a decision, and the operator makes it.
-- **Unknown is left alone.** A record written before the host kept what it found says nothing
- about the unit before the mesh; the unit is left exactly as it is. A unit left running can be
- stopped by the operator; one stopped by mistake may be the link the operator needed to do it.
-- A unit the mesh *did* create — a `process` resource's unit and bundle — is stopped and removed
- with its declaration. That is the mesh's own code. (Before this record there was no way to
- remove one at all: an undeclared process failed every apply on its node.)
-- The service's settings the mesh wrote are given back by their own resources (a kept original
- restored, a region or keys removed). A running service keeps running on what it read until it
- next reads its configuration; the mesh does not restart it to make it notice.
-- A service declared with no `state` (ADR 0125) remains the way to say the mesh must not
- **start** a unit either; undeclared, it is forgotten.
-
-## Consequences
-
-- Unassigning the private network no longer stops the container runtime; unassigning sshd no
- longer stops ssh; no uplink module can take a machine's network down on its way out.
-- The host's removal report says what it gave back — "restored: stopped again, as the host
- found it" — or "forgotten: it was running before the mesh; left as it is" where it used to say
- "stopped". Its plan names each unit an undeclare will stop, before it does.
-- On a fresh machine where the mesh installed and started a service, unassigning its module
- stops it again — the mesh gave, the mesh takes back. An operator who wants it kept declares it
- in a module of their own, or starts it themselves after.
-- A daemon can keep running after its module is gone, on configuration that was taken back from
- under it. That is a visible, running process the operator can see and stop; the alternative
- was an invisible outage.
-- **Not decided here:** an unassign preview that lists what an undeclare will remove and what it
- will leave running. Issue 130 asks for it; it is the controller's to build.
-
-## References
-
-- [issue 130](../04-ISSUES/130-undeclaring-a-service-stops-it/00-report.md): the finding
-- [ADR 0125](0117-a-machines-uplink-is-a-seat.md): the uplink modules, and a service with no state
-- [ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md), [ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md):
- what is given back, and how
-- mesh-host `internal/apply/apply.go` (`remove`, the service case)
diff --git a/02-DECISIONS/0119-a-taken-tunnels-predecessor-is-retired.md b/02-DECISIONS/0119-a-taken-tunnels-predecessor-is-retired.md
deleted file mode 100644
index 4b57cb72..00000000
--- a/02-DECISIONS/0119-a-taken-tunnels-predecessor-is-retired.md
+++ /dev/null
@@ -1,87 +0,0 @@
----
-topic: the mesh
-status: accepted
-date: 2026-09-27
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0105-the-mesh-adopts-the-predecessors-tunnel-in-place.md
----
-
-# 119. A taken tunnel's predecessor is retired once the take is proven
-
-## Context
-
-[ADR 0105](0105-the-mesh-adopts-the-predecessors-tunnel-in-place.md) has the private network take
-over the tunnel it finds: the found unit stopped and disabled, never flushed, and **its
-configuration left on disk, kept like any held file.** That was the right caution for the take
-itself — if the mesh's interface failed to come up, the host starts the found unit again and the
-peers never notice — and every apply since stops the found unit again should anyone start it.
-
-What it leaves is a predecessor that never finishes leaving. On every machine that has enrolled,
-the tunnel is the mesh's and has been proven so — its interface up with the found key, the peers
-handshaking, the machines resolving and reaching each other over it — and still the predecessor's
-configuration sits where its unit reads it, held for a module that has long since replaced it.
-The predecessor itself is being deprecated. A tunnel that can be started again by one command, with
-a configuration nothing maintains any more, is not a rollback path; it is a second way onto the
-network that nobody is watching. And the hold never ends, so every node report keeps listing it.
-
-## Considered Options
-
-**1. Keep it, as 0105 says.** Rejected: the caution it bought is spent once the take is proven, and
-what remains is a live, unmaintained way back onto the network.
-
-**2. Delete it at the take.** Rejected: the take is exactly the moment the fallback is needed. If
-the mesh's interface does not come up, the host must still be able to raise the found one.
-
-**3. Retire it once the take is proven.** Chosen.
-
-## Decision
-
-**Once the mesh's interface has proven it carries the tunnel, the found interface's configuration
-is removed from where its unit reads it.**
-
-- **Proven means:** the tunnel's state is *taken* — the found unit down and disabled, the mesh's
- interface up with the found key — and the mesh's interface has completed a handshake with at
- least one peer. Not before: until then, a failed take still falls back to the found unit.
-- **Retired means:** the configuration file the found unit reads is removed. Its original was
- already kept, before anything happened to it
- ([ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md)), and stays kept; that copy
- is the record of what the predecessor was, and a person's way back if one is ever wanted.
-- The found unit stays disabled. Without its configuration it cannot raise the interface, so the
- every-apply stop that guarded against it becomes a check that finds nothing to do.
-- **The hold ends.** What was held for the private network has been replaced; the node stops
- reporting it.
-- **The mesh never brings it back.** Undeclaring the private network does not restore the found
- tunnel: the mesh stopped it, and nothing is started on the way out
- ([ADR 0126](0118-undeclaring-gives-a-unit-back-the-state-it-was-found-in.md)). A machine whose
- private network is unassigned has no tunnel until it is assigned again — which is what
- unassigning it means.
-
-## Consequences
-
-- On every machine that took a tunnel, the predecessor's tunnel configuration disappears at the
- first apply after the take is proven. Nothing a peer sees changes; the mesh's interface already
- carries the same key, port, address and peers.
-- A take that is never proven — no peer ever handshakes — keeps the found configuration, and the
- node says so, so a broken take is visible rather than silently retired.
-- Rolling back to the predecessor's tunnel becomes a deliberate act, in this order: **unassign the
- private network first**, then copy the kept original back and start its unit. The mesh does
- neither. While the private network is still assigned, the tunnel is the mesh's: a restored
- configuration is held and retired again at the next proven apply, and the found unit cannot
- bind the port the mesh's interface holds. The node says so when it happens.
-- A configuration something keeps writing back — the predecessor's own tooling, say — is retired
- again each time it appears, but the first original stays the one kept; a different content is
- kept once beside it, and the node reports that the configuration came back.
-- The host retires only the found interface's own configuration file (`/etc/wireguard/.conf`),
- never a path the mesh writes, and never a link: a configuration that is a link to somewhere else
- is left, with its target, for a person to retire.
-- 0105's "its configuration stays on disk, kept like any held file" holds until the take is proven,
- and not after.
-
-## References
-
-- [ADR 0105](0105-the-mesh-adopts-the-predecessors-tunnel-in-place.md): the take, and why it keeps
- the found configuration during it
-- [ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md): kept originals
-- [ADR 0126](0118-undeclaring-gives-a-unit-back-the-state-it-was-found-in.md): nothing is started on the way out
-- mesh-host `internal/apply/takeover.go`
diff --git a/02-DECISIONS/0120-a-roster-fact-carries-its-format-as-a-template.md b/02-DECISIONS/0120-a-roster-fact-carries-its-format-as-a-template.md
deleted file mode 100644
index 7bcc58c0..00000000
--- a/02-DECISIONS/0120-a-roster-fact-carries-its-format-as-a-template.md
+++ /dev/null
@@ -1,136 +0,0 @@
----
-topic: what runs on it
-status: accepted
-date: 2026-09-27
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
----
-
-# 120. A roster fact carries its format as a template: the mesh owns the data, the module owns the format
-
-## Context
-
-A **fact** is a thing only the mesh knows — which machines exist, what they are called, where they
-are — written into a file where a module asks for it. The mesh computes it from the graph; a module
-loads it, restarts on it, does what its software does with it. Facts replaced three modules that
-existed only because computed output needed somewhere to live and ran no software of their own
-([ADR 0040](0040-what-a-module-is.md)).
-
-But the *format* lived in the control plane. A fact was a name from a closed list, and each name had
-a formatter written in Go beside the others: `node-names` wrote the roster as an `/etc/hosts` file,
-`node-zones` wrote it as a dnsmasq resolver's `local=`/`address=` lines. Adding a consumer meant
-adding a formatter — in the consumer's own configuration language — to the mesh.
-
-The ssh work made the cost plain. An operator's `~/.ssh` wants three roster projections — a
-`known_hosts`, an ssh `config` of `Host` blocks, an `authorized_keys` — each in ssh's syntax. Under
-the closed list that is three more formatters in the control plane, teaching it ssh's configuration
-language. And it does not stop at ssh: every daemon that reads the roster in its own file format
-would put its grammar here. The control plane was accreting the configuration languages of software
-it does not run — the exact thing [ADR 0040](0040-what-a-module-is.md) says is a
-module's and not the mesh's.
-
-The shape underneath is one shape. WireGuard's `[Peer]` blocks, `/etc/hosts`, dnsmasq's zones, an
-ssh `known_hosts` — all of them are *the roster, projected into a file*. Only the projection differs,
-and the projection belongs to whoever runs the software that reads it.
-
-## Considered Options
-
-**1. Keep the closed list; add a formatter per consumer.** Rejected. The control plane learns the
-configuration language of every daemon any module might run, without bound, and each format lives in
-the mesh rather than in the module that owns the file. A module cannot change how its own file is
-written without a control-plane change.
-
-**2. A general placeholder vocabulary over `content`, like `${machine:address}` but for the
-roster.** Rejected. The mesh's other substitutions each resolve to *one* scalar — this machine's
-address, one provider's port. The roster is inherently a *repetition*: one block per machine. A flat
-`${…}` vocabulary cannot iterate, and a mechanism that could would be a template in all but name.
-
-**3. The module gives a path and a template over the roster; the mesh renders it.** Chosen. The mesh
-owns the data — who exists, their names and addresses — and hands it to a Go `text/template` the
-module wrote. The mesh renders and reads neither the template's intent nor the file's meaning.
-
-## Decision
-
-**A fact is a path and a template.** In a module's manifest, `facts` maps a name the module chooses
-to a `{ path, template }`. The template is a Go `text/template` over a fixed **roster view**:
-
-- `.Node` — this machine's bare name.
-- `.Suffix` — what a mesh name ends in (`internal`, or the operator's choice), as composed.
-- `.Names` — every name the mesh serves: the machines *and* the names it was told to route.
-- `.Machines` — only the machines that are nodes of this mesh.
-
-Each of `.Names` and `.Machines` is a list of `{ Name, FQDN, Address }`. A machine the mesh has a
-record for but cannot yet place has no address and is left out of both — a name that resolves to
-nothing is a connection that hangs, so it is omitted rather than written (the same rule as before).
-
-**The mesh owns the data; the module owns the format.** The control plane holds **no** formatter.
-The two built-in projections render through the same path any module uses:
-
-- **`/etc/hosts`** is a template on the mesh's own network module. The mesh writes `/etc/hosts`
- because being on the private network is what gives a machine a name — but the *layout* is a
- template like any other, shipped with the control plane because that module ships with it, not
- because the control plane knows the hosts-file format.
-- **dnsmasq's zones** move into dnsmasq. The `local=`/`address=` grammar is dnsmasq's configuration
- language, and it now lives in dnsmasq's manifest, where the module that runs dnsmasq owns it.
-
-**A fact also says whether its file is the mesh's whole or a region of the machine's.** A hosts file
-is the machine's — its `localhost`, the operator's lines, another tool's marked blocks — so
-`node-names` is `shared`: the mesh owns only its region and keeps the rest byte for byte, the host
-laying it down `into: block`
-([ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md), hq issue 128). A resolver's
-zones file is the mesh's whole, and is not shared. The template renders the content either way;
-`shared` decides how the host writes it. This composes with hq 128 rather than replacing it: the
-region *mechanism* is the host's, the region's *format* is the module's template.
-
-**The names-vs-machines distinction is the template's choice** ([04-ISSUES/111](../04-ISSUES/111-the-resolver-is-told-names-the-mesh-serves-not-only-machines/00-report.md)):
-a container's hosts ranges `.Names`, so a routed name resolves to the machine serving it; a resolver
-told the mesh's suffix is its own ranges `.Machines`, or a routed name written there with the suffix
-appended is a name nobody will ever ask for.
-
-**A template that will not render is refused at composition, not on a machine.** A template that does
-not parse, or reads a field the roster does not have, fails where the manifest is — the closed-list
-safety, moved from the fact's *name* to the roster's *shape*. A daemon that starts, reads a file the
-mesh could not render, and answers nothing is a much worse way to find out.
-
-**WireGuard stays a computed generator, and that is the line.** Its `mesh0.conf` is not a pure roster
-projection — it carries topology the control plane decides: which peers are reachable, endpoints, hub
-forwarding, keepalive for a NAT'd node. And it is *foundational*: the overlay must be up before any
-module can be delivered, so the thing that writes it cannot itself be a delivered module. The line
-this draws: **the substrate that delivery rides on is the control plane's; everything layered on a
-working overlay is a roster template.** DNS, hosts, and ssh are layered; the overlay is the floor.
-
-## Consequences
-
-- **ssh is two templates and no control-plane change.** Once the roster view carries a machine's ssh
- host key and its operator account ([to-be 29](../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md)),
- `known_hosts`, the ssh `config`, and `authorized_keys` are templates on the ssh modules — the mesh
- gains no knowledge of ssh's syntax. This ADR is what makes that work land without touching the
- controller.
-- **A new roster projection never touches the control plane.** Any module that reads the roster in
- its own format ships its own template.
-- **A module can change how its own file is written** without a control-plane change — it is editing
- its own manifest.
-- **The schema changed and is not backward compatible.** A fact was a string (a path); it is now
- `{ path, template }`. The old string form has no template and cannot be auto-upgraded, because the
- format it implied was the formatter this ADR deletes. The controller and every catalogue module
- using facts — only dnsmasq — land together. A controller and a catalogue that disagree cannot
- compose the module: the running daemon on a machine is unaffected, but the mesh will not send it a
- new declaration until both sides agree.
-- **The output did not change.** The `/etc/hosts` and dnsmasq zones a machine receives are
- byte-for-byte what the deleted formatters wrote, pinned by tests that render the built-in template
- and compose the real dnsmasq manifest.
-
-## References
-
-- [ADR 0040](0040-what-a-module-is.md): a module is software the mesh runs — a
- format the mesh knows for software it does not run was the accretion this stops
-- [ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md): a module definition names no
- path; this is its sibling for content — a module definition names no format the mesh must know
-- [to-be 29](../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md): the ssh consumer this
- unblocks, and the roster fields it will add
-- [04-ISSUES/111](../04-ISSUES/111-the-resolver-is-told-names-the-mesh-serves-not-only-machines/00-report.md): every served name is not a
- machine — now the template's choice of `.Names` or `.Machines`
-- mesh-controller `internal/catalogue/roster.go` (the mechanism), `internal/overlay/generator.go`
- (the built-in `/etc/hosts` template), `internal/catalogue/manifest.go` (`RosterFile`)
-- mesh-catalog `modules/dnsmasq/module.json` (the zones template, dnsmasq's own)
diff --git a/02-DECISIONS/0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md b/02-DECISIONS/0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md
deleted file mode 100644
index 2c684b24..00000000
--- a/02-DECISIONS/0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md
+++ /dev/null
@@ -1,146 +0,0 @@
----
-topic: what runs on it
-status: accepted
-date: 2026-09-27
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md
----
-
-# 121. A system seat is named for its scope, and a module may define its own
-
-> **Superseded in part — 2026-10-03, by [ADR 0194](0194-the-mesh-has-one-resolver-and-every-node-asks-it-for-the-meshs-names.md).**
-> *"`the-dns-port` → `node-dns-resolver`"* no longer holds: the serving role moves to mesh scope as
-> `mesh-resolver`, one per mesh, and `node-dns-resolver` is retired. The distinction this record kept —
-> serving and asking are two roles, two seats — stands, and `node-resolver-config` is unchanged.
-
-> **The mechanism changed — 2026-10-02, by [ADR 0190](0190-a-seats-work-is-shared-by-its-holders-and-building-is-the-first-such-role.md).** The naming rule stands. The build role this record made mesh-scoped — *the mesh's single build machine* — is node-scoped now: `node-build-agent`, one holder per machine, every holder taking from one work queue.
-
-> **Superseded in part — 2026-09-27, by [ADR 0122](0122-a-seat-is-data-a-rename-is-a-database-update.md).**
-> "The renames are a coordinated migration, not a rename" no longer holds: a seat is a row in the controller's
-> store, referenced by a stable id, so a rename is one update of its name, and only the claiming manifests are
-> edited in the catalogue. The naming rule, the module-defined seats and the seats settled here stand.
-
-## Context
-
-[ADR 0110](0110-a-seat-is-a-module-assignment-from-a-closed-set.md) made seats a closed set the
-control plane defines: a well-formed name no longer becomes a seat by being claimed, so a person can
-read what a mesh can have and who fills each role. It left two things unsettled that the growing set
-now exposes:
-
-- **The names carry no rule.** `mesh-controller`, `mesh-store`, `mesh-broker` are named for the mesh;
- beside them sit `the-artifact-store`, `the-build-machine`, `the-dns-port`, `the-showcase`,
- `the-uplink` — a second naming style with no principle behind it. A reader cannot tell a seat's
- scope from its name, and the mesh's own roles do not look like the mesh's.
-- **The set is the *only* place a seat may be defined.** A module claiming any name not in the
- control plane's set is refused. That is right for *system* roles — one broker, one packet filter
- per node — but it means a module can never define a role of its own: a demo module's
- `the-showcase`, a future application's coordination role, must be smuggled into the control plane's
- set or not exist. The control plane ends up holding roles that are not the mesh's to define.
-
-Reviewing the set against these also found seats whose *scope* or *membership* is wrong, not just
-their name — the review is the occasion to fix those too.
-
-## Decision
-
-**A system seat — one the control plane defines — is named for its scope:**
-
-- **`mesh-*`** for a mesh-scoped seat: one holder in the whole mesh, a role the mesh has once
- (`mesh-controller`, `mesh-store`, `mesh-broker`, `mesh-git`, …). A `mesh-*` seat is always held by
- a module **on a named node** — `mesh-git` is gitea *on novox*, not "gitea"; another node running
- gitea does not hold `mesh-git` unless it is the holder. The seat is the mesh's single answer for
- the role, and which node answers is part of what the seat records.
-- **`node-*`** for a node-scoped seat: one holder per node, a role each machine has at most once
- (`node-packet-filter`, `node-intrusion-prevention`, `node-uplink`, …).
-
-The three already-`mesh-*` seats keep their names; the rest are renamed by this rule. The scope a
-name declares must match the seat's actual scope — a `mesh-*` seat at node scope, or the reverse, is
-a contradiction the reader is entitled to trust is impossible.
-
-**The control plane defines only system seats. A module may define its own.** A seat named `mesh-*`
-or `node-*` is the control plane's, and claiming one the control plane does not define is refused as
-before. Any *other* name is a **module-defined seat**: valid when the module declaring the claim also
-declares the seat (its name, scope, and — if any — the protocol its holder speaks). The control plane
-enforces one-holder-per-scope for it exactly as for its own, but does not otherwise know what it
-means. So an application can coordinate its own instances through a seat of its own, and the mesh's
-closed set stays what its name says it is: the *system's* roles, not everyone's.
-
-**Specific seats this settles:**
-
-- **`the-build-machine` → `mesh-build-machine`, and its scope becomes mesh.** There is one build
- machine in the mesh (the builder on novox), not one per node. Node scope said the opposite. It
- delivers no provision; it is the mesh's single build machine.
-- **`the-private-network` → `mesh-private-network`, held by the network *server* on one node.** Today
- it is node-scoped and held on every node, with a stated (untested) story that a different VPN could
- hold it per machine — which would force every provider module to independently implement receiving
- and applying the controller-composed configuration. The mesh does not work that way and should not
- pretend to: **one mesh decides one private network.** The seat is mesh-scoped, held by the server
- module (WireGuard on the hub, novox). A machine that joins is given a **client module** that
- receives the composed configuration and applies it; when a node joins, the mesh emits each node's
- configuration so all of them know each other at once. This drops per-node VPN choice deliberately —
- the private network is nox-mesh's own, and it defines the nodes' configuration rather than being
- assembled from each node's opinion. (Implementation: the overlay generator's per-node computation
- is unchanged; what changes is the seat's scope and the server/client split of the module.)
-- **`the-showcase` → removed from the set; it becomes a module-defined seat.** It is a demo module's
- own coordination role, claimed by nothing else and held nowhere. It is the first module-defined
- seat, and the reason the rule above is needed rather than hypothetical.
-- **`the-dns-port` → `node-dns-resolver`** (the daemon that binds `:53`), kept distinct from
- **`the-resolver-configuration` → `node-resolver-config`** (what writes `resolv.conf`). Two roles,
- two seats; the rename must not blur them.
-- **`the-packet-filter` → `node-packet-filter`** and **`the-intrusion-prevention` →
- `node-intrusion-prevention`** — names kept as-is but for the prefix. "Packet filter" stays distinct
- from "firewall", which would swallow intrusion-prevention too.
-- **`the-uplink` → `node-uplink`** ([ADR 0125](0117-a-machines-uplink-is-a-seat.md)). Unheld, so it
- renames with no migration.
-- **The registry seats — `the-artifact-store`, `npm-package-registry` (→ `mesh-artifact-store`,
- `mesh-npm-package-registry`) — and `git` (→ `mesh-git`) — are decided but deferred.** *(The
- mechanism changed — 2026-09-30, by ADR 0156: `the-artifact-store` is renamed, one update and one
- alias under ADR 0122; the other two stay deferred.)* They each
- *deliver* a provision, so renaming them is a delivering-seat migration: a holder that stops
- resolving mid-flight takes a provision away from every consumer. That risk is not worth carrying in
- the same pass as the node-* renames, so they keep their names until done deliberately.
-
-**`distribution` stays the mesh's registry; only `verdaccio` is retired.** An earlier draft of this
-record had the registry consolidating onto gitea and `distribution` retired — that was reversed:
-`distribution` is the standalone OCI registry serving every `artifact-store://…@sha256` image (the
-control plane's own included), and the mesh keeps it. `verdaccio` was a *second* npm registry;
-gitea already provides `npm-package-registry`, so verdaccio is redundant and is removed. It is only
-in the catalogue (never registered in the running mesh), so removing it is deleting the module — no
-migration, nothing to strand.
-
-## Consequences
-
-- **A reader learns a seat's scope from its name.** `mesh-*` is mesh-wide and one; `node-*` is
- per-machine. The mesh's own roles finally look like the mesh's.
-- **Applications get their own seats** without the control plane learning their meaning. The closed
- set shrinks to what it should be — the system's roles — and stops being where unrelated roles hide.
-- **The renames are a coordinated migration, not a rename.** A held seat's name lives in three places
- that must move together: the control plane's set (`seats.go`), every claiming manifest, and what
- each node reports it holds (re-derived by re-registering the manifest and re-pushing). A seat
- renamed in one place and not the others stops resolving to its holder — and for a *delivering* seat
- (`mesh-store`→postgres, `mesh-broker`→amqp, the registry seats) that is a mesh-wide provision
- outage, the same failure mode as a schema change hitting an old manifest. So: the non-delivering
- `node-*` seats and `mesh-build-machine` migrate as one tested controller+catalogue change;
- `node-uplink` is free (unheld); the delivering registry seats are deferred to their own pass.
-- **The node-* migration was done as one controlled step, and it froze briefly.** Deploying the new
- controller made it reject the still-old-named claims in the stored manifests, so composition stopped
- for the affected nodes until each manifest was re-registered under its new name; running services
- were untouched, and the window was seconds. This is the coordinated-migration cost named above,
- paid once — and the reason the *delivering* registry seats, whose freeze would be a provision
- outage rather than a compose pause, are not folded into the same pass.
-- **`distribution` is not retired.** It stays as the registry; only `verdaccio` (a redundant second
- npm registry) is removed. The mesh keeps one OCI registry (`distribution`) and gitea for npm/git —
- the "one registry, on gitea" idea was considered and dropped.
-- **The private network stops pretending to be swappable per node.** The gain is a coherent
- server/client model matching how the controller already composes configuration; the cost is that
- choosing a different VPN is now a mesh-wide change, not a per-node one — accepted.
-
-## References
-
-- [ADR 0110](0110-a-seat-is-a-module-assignment-from-a-closed-set.md) — the closed set this refines
-- [ADR 0125](0117-a-machines-uplink-is-a-seat.md) — `the-uplink`, renamed here to `node-uplink`
-- [ADR 0079](0079-the-foundation-seats-are-named-after-their-servers.md) — the original `mesh-*` seats
- whose naming this generalises
-- [to-be 26](../03-DESIGN/01-to-be/26-the-seats.md) — the seat table, updated by this
-- mesh-controller `internal/catalogue/seats.go` (the set and claim validation),
- `internal/overlay/generator.go` (the private network as server + client)
diff --git a/02-DECISIONS/0122-a-seat-is-data-a-rename-is-a-database-update.md b/02-DECISIONS/0122-a-seat-is-data-a-rename-is-a-database-update.md
deleted file mode 100644
index 3676bdf0..00000000
--- a/02-DECISIONS/0122-a-seat-is-data-a-rename-is-a-database-update.md
+++ /dev/null
@@ -1,107 +0,0 @@
----
-topic: what runs on it
-status: accepted
-date: 2026-09-27
-deciders: jochen
-reconstructed: false
-supersedes-in-part:
- - 02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md
- - 02-DECISIONS/0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md
----
-
-# 122. A seat is data the controller owns, and a rename is a database update
-
-## Context
-
-[ADR 0110](0110-a-seat-is-a-module-assignment-from-a-closed-set.md) made the seats a closed set the
-control plane defines, and [ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md)
-named them by scope. Both were right about *what* a seat is. Both left it defined the wrong *way*:
-**the set is a hardcoded Go slice compiled into the controller, and everything references a seat by
-its name as a string literal.** Renaming `the-packet-filter` to `node-packet-filter` this session
-took, in one pass:
-
-- an edit to the Go slice in `internal/catalogue/seats.go`, recompiled into a new controller image;
-- an edit to a `const gitSeat = "git"` in *production* control-plane code (`source.go`), because a
- seat's name was hardcoded where a repository's home is resolved;
-- edits to every claiming manifest in the catalogue, each re-registered;
-- a controller **rebuild and redeploy**, which — because the running controller then refused the
- still-old-named claims in stored manifests — **froze composition** for the affected nodes until
- each manifest was re-registered under its new name;
-- the same coupling in the **build machine**, which embeds the same seat set and refused to build
- anything claiming a name it did not yet know;
-- a **deadlock** when the build machine's own seat was renamed, since the old builder could not
- build the new builder whose manifest claimed a name it rejected.
-
-None of that is what a rename should cost. A rename is the operator changing a label. It should be a
-single write, and nothing should have to be rebuilt, refused, or unfrozen. The set being *closed*
-(0110) and *named by scope* (0121) are good rules; **the set being code is the mistake.** When
-adhering to the design means twenty steps and a `const` in the resolver, the design is what to fix.
-
-## Decision
-
-**The seat set is data the control plane owns, not code it is compiled from.** The seats live in a
-table in the controller's store — one row per seat: a **stable id**, a `name`, a `scope`, what it
-`delivers` (a provision, or nothing), and the record that decided it. The rows are seeded by a
-migration (the closed set 0110 defines still ships with the mesh), and thereafter they are ordinary
-data the control plane reads and writes.
-
-**A seat is referenced by its stable id, never by its name.** A claim, a held-seat record, and any
-control-plane code that must name a seat (the git-seat resolver, the artifact-store guard) hold the
-**id**. The `name` is a label for people and for what a manifest writes; it is resolved to an id
-once, when a claim is registered. So:
-
-- **A rename is one `UPDATE seats set name = … where id = …`.** Nothing is recompiled, nothing is
- re-registered, nothing is refused, nothing freezes. Held records and claims already point at the
- id, so they follow the rename for free. The build machine is not involved, because the build
- machine validates a claim against the set it reads from the mesh, not one baked into its image.
-- **Adding or removing a seat is an `INSERT`/`DELETE`** (within the closed-set discipline: a change
- to the set is still a decision with a record — the record is now a row's `decided` column and an
- ADR, not a line of Go). No controller release is needed to change the roster of roles.
-- **Production code stops hardcoding names.** `const gitSeat = "git"` becomes a lookup of the seat
- that delivers the `git` provision (or a well-known id), so renaming its label cannot break the
- code that finds a repository's forge.
-
-**What does not change** (0110 and 0121 still hold): a seat is still a module assignment from a
-closed set; there is still one holder per scope; a delivering seat is still the single answer for
-its provision; system seats are still `mesh-*`/`node-*` and a module may still define its own. Only
-their *storage and reference* change — from a compiled slice keyed by name to a table keyed by id.
-
-**A manifest still claims by name, and that is fine.** A manifest is written by a person and names
-the seat in words; the mesh resolves the name to an id at registration and stores the id. If a
-seat's name changes, manifests written against the old name are updated in the catalogue like any
-other edit (and the mesh can keep the old name as an alias row during a transition so nothing breaks
-in the window) — but the *control plane* never has to change or redeploy for it, which is the whole
-point. The heavy, mesh-wide, freeze-prone half of a rename disappears; only the ordinary catalogue
-edit remains.
-
-## Consequences
-
-- **A rename, and a set change, become operations, not releases.** The pain this session paid —
- three freezes, a builder deadlock, hand-resolved manifests — is designed out. The seat migrations
- still outstanding (the delivering registry seats, and the private network's scope change) should
- wait for this: done as data, each is a write, not a coupled multi-repo deploy.
-- **The controller gains a small table and a seed migration**, and its seat lookups change from
- slice scans to id-keyed reads. `SeatNamed`, `SeatDelivering`, `claimProblems` read the table.
-- **The build machine reads the set from the mesh** (it already talks to the control plane), rather
- than embedding it — which removes the controller/builder seat coupling that made every breaking
- seat change a two-sided deadlock (see [to-be 30](../03-DESIGN/01-to-be/30-the-mesh-updates-itself-on-a-push.md)).
-- **The closed set is still closed.** Data being editable is not the set being open: changing it is
- still a decision, still recorded. What changes is that recording it no longer means shipping a
- binary.
-- **This is a real refactor**, touching the store schema, the seat lookups, claim registration
- (name→id resolution), and the held-seat records. It is worth its own build; until it lands, the
- current compiled set stands and further renames are held rather than forced through the heavy path.
-- **Config on a seat is still the module's** (the question that surfaced this): a seat row carries
- the seat's own metadata (scope, delivers, protocol), not a module's configuration — that stays in
- the holding module's manifest ([ADR 0046](0046-a-module-configuration-is-its-assignments-not-its-manifest.md)). Making
- seats data does not make them a config store.
-
-## References
-
-- [ADR 0110](0110-a-seat-is-a-module-assignment-from-a-closed-set.md),
- [ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md) — the seat
- rules this keeps, whose *storage* it changes
-- [to-be 30](../03-DESIGN/01-to-be/30-the-mesh-updates-itself-on-a-push.md) — the controller/builder
- seat coupling and the breaking-change freeze this removes for seat changes
-- mesh-controller `internal/catalogue/seats.go` (the compiled slice this replaces),
- `cmd/mesh-controller/source.go` (`const gitSeat`, the hardcoded name this removes)
diff --git a/02-DECISIONS/0125-the-bus-is-the-only-broker.md b/02-DECISIONS/0125-the-bus-is-the-only-broker.md
deleted file mode 100644
index be960e88..00000000
--- a/02-DECISIONS/0125-the-bus-is-the-only-broker.md
+++ /dev/null
@@ -1,133 +0,0 @@
----
-topic: the mesh
-status: superseded
-superseded-by: 02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md
-date: 2026-09-26
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0106-the-bus-is-nats.md
----
-
-# 125. The bus is the only broker
-
-## Context
-
-[ADR 0106](0106-the-bus-is-nats.md) moved the mesh's bus to NATS and kept the AMQP broker "as a
-module with one purpose — the predecessor's clients", retiring with the last of them.
-[Design 25](../03-DESIGN/01-to-be/25-the-bus-on-nats.md) repeats that: a compatibility module with
-a single purpose and a retirement condition.
-
-**It is not single-purpose, and was not when that was written.** Two modules of the *new* mesh
-declare `requires: ["amqp"]` and are answered by the broker module's own provisioner:
-
-- `amqp-ping`, whose source says it "exists to PROVE the grant end to end: the mesh gave it a
- scoped login and a vhost of that name on the lavinmq provider";
-- `amqp-email-forwarder`, which uses it for work.
-
-What that provisioner answers is **not the mesh's bus**. Its own comment draws the line: a
-consumer gets "its own message broker, isolated from every other consumer's by the vhost
-boundary… a broker of its own, not a shared account on the mesh's control-plane broker" —
-vhost-per-login, "the exact analog of postgres's database-per-login."
-
-So two different things wear the word *broker*: the mesh's nervous system, and a private message
-broker handed to a module as a resource, the way a database is. The first is being replaced. The
-second was never examined, and on the retirement condition ADR 0106 sets, it disappears with no
-successor and nothing notices — a module of the new mesh left requiring something no provider
-answers.
-
-The operator's direction, asked at the point this surfaced: **NATS is the heart of the
-application** — not a component it contains, and not a thing to reproduce the predecessor's
-shapes on.
-
-## Considered Options
-
-1. **Carry the private broker forward onto NATS** — each requiring module gets its own NATS
- account, provisioned like a database. Rejected on three counts. It reproduces the
- predecessor's shape on the new bus, which is the thing this whole move exists to stop. It
- gives the mesh two messaging models, so "how does a module send a message" has two answers
- depending on a manifest line. And NATS accounts isolate subject spaces *entirely*: a module
- inside its own account cannot reach the mesh's bus at all, so it would hold two connections
- and two identities to do one job.
-2. **Keep the compatibility broker indefinitely** for the mesh's own modules. Rejected: its
- retirement condition is the point of it. A module of the new mesh depending on the retired one
- keeps the predecessor alive permanently, which is the opposite of a compatibility module.
-3. **One bus. A module's messaging is subjects on it, scoped by what it declares.** Adopted.
-
-## Decision
-
-**The bus is the only broker.** NATS is the mesh's one messaging system, and every module's
-messaging is subjects on that bus under its own account, scoped by its `emits` and `consumes`
-([ADR 0043](0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md)). There is no second
-broker, and none is handed to a module as a resource.
-
-**The `amqp` interface is not carried forward.** It leaves the set of things a module may require
-and retires with the compatibility broker rather than gaining a successor.
-
-Concretely, in the controller's seat table: **the `mesh-broker` seat delivers nothing.** It
-currently reads `Delivers: "amqp"` — the seat's holder answers a requirement for a broker — and
-under this decision it joins `mesh-controller` and `the-catalogue`, the foundation seats that
-deliver no provision at all. The bus is not something a module asks for; it is what a module is
-reached through.
-
-- `amqp-email-forwarder` moves to the bus like any module: what it emits and consumes, declared,
- and the account follows.
-- `amqp-ping`'s *purpose* is kept and its mechanism is not. Proving end to end that a module
- receives scoped messaging it did not configure itself is worth a probe; it becomes a probe of
- the bus, and its assertion changes from "I reached my own vhost" to "I reached exactly my
- subjects and was refused the rest."
-
-**A module that wants a queue of its own has one already**: a subject nothing else may publish to
-and a durable consumer of its own, both derived from its declaration. What it does not get is a
-server of its own.
-
-**The mesh's own streams are the controller's, created at genesis, not provisioned** — and
-`EVENTS` is one stream, closing the question [design 25](../03-DESIGN/01-to-be/25-the-bus-on-nats.md)
-§11 left open. The reason is not preference but **bootstrapping**: a provisioner is a module, and
-a module needs a bus account before it can run at all. Anything the bus itself is made of must
-exist before the first module starts, so it is composed as configuration
-([ADR 0106](0106-the-bus-is-nats.md): never through a management API) rather than provisioned by
-something that could not yet be running.
-
-## Consequences
-
-- **Design 25 gains the distinction and loses the "single purpose" claim**; its §11 question about
- the `EVENTS` stream closes here.
-- **Nothing in [design 27](../03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md)'s
- model changes** — the four provider kinds, the contract, resolution all stand, and it never
- enumerated interfaces, so there is nothing to strike from it. What changes is that messaging
- leaves the set of things resolved at all: every module has it by existing.
-- **One line of the controller's seat table changes**, and it is the load-bearing one:
- `mesh-broker` stops declaring what it delivers. A requirement for `amqp` then resolves to
- nothing and is refused at assignment, which is how the two modules below are found rather than
- discovered at runtime.
-- **Two modules have conversion work**, and it belongs to step 4 of
- [ADR 0116](0116-the-bus-is-built-in-five-steps.md), with the flows. Neither blocks step 1.
-- **The compatibility broker becomes what ADR 0106 already called it** — single-purpose — once
- those two have moved. That record's claim was wrong when written and is made true by this one.
-- **What got harder:** a module that genuinely wanted an isolated server — a tenant boundary at
- the broker rather than at the subject — no longer has that option, and would have to argue for
- it as a new decision. That is the intended cost: one bus is the point.
-
-## How it is checked
-
-- **A module's messaging works with no `requires` line for it.** A lab bed: a module declaring
- only `emits` and `consumes` reaches its subjects, and is refused every other — which is
- [ADR 0116](0116-the-bus-is-built-in-five-steps.md) step 1's permission bed, already required.
-- **Nothing requires `amqp`.** With the seat delivering nothing, a module still declaring it is
- refused at resolution — the existing "requirement no provider answers" path, not a new check. A
- catalogue test asserts no module declares it once the two have moved.
-- **The probe proves the claim it is named for.** `amqp-ping`'s successor fails if a module can
- reach a subject outside its declaration, not merely if it cannot reach its own.
-- **The compatibility broker's retirement condition can actually be met.** A check that no module
- of the mesh — as opposed to a predecessor client — holds a connection to it.
-
-## References
-
-- [ADR 0106](0106-the-bus-is-nats.md) — the bus is NATS; corrected here on what the compatibility
- broker serves.
-- [ADR 0116](0116-the-bus-is-built-in-five-steps.md) — the steps; the conversions land in step 4.
-- [ADR 0043](0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md) — the scoping that
- makes one bus safe.
-- [design 25](../03-DESIGN/01-to-be/25-the-bus-on-nats.md),
- [design 27](../03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md) — the two documents
- this changes.
diff --git a/02-DECISIONS/0126-a-module-declares-its-own-seats.md b/02-DECISIONS/0126-a-module-declares-its-own-seats.md
deleted file mode 100644
index 8429294a..00000000
--- a/02-DECISIONS/0126-a-module-declares-its-own-seats.md
+++ /dev/null
@@ -1,145 +0,0 @@
----
-topic: the tiers
-status: accepted
-date: 2026-09-26
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md
----
-
-# 126. A module declares its own seats; the mesh reserves its own
-
-## Context
-
-[ADR 0110](0110-a-seat-is-a-module-assignment-from-a-closed-set.md) closed the set of seats. Its
-evidence was strong and still is: nothing could answer *which seats does this mesh have, and who
-holds each*. Answering it meant reading every manifest in two repositories and then the
-controller's own code, and when that enumeration was done by hand while writing the record, **it
-reported eleven claims where there were thirteen.** The fix was a table in the controller, and
-adding a seat became a decision.
-
-What that table cannot express is the architecture [ADR 0125](0125-the-bus-is-the-only-broker.md)
-opened. With one bus and no private brokers, a module offering a service to other modules offers
-it as **a role on the bus**: a set of subjects, exactly one holder, addressed by what it does
-rather than by which module or node provides it. A telegram sender, a licensing master, anything
-a mesh might want one of. Under a closed table, adding any of those means editing the controller
-— so a capability contributed by a module would require a change to the mesh itself, which is the
-coupling the module system exists to prevent.
-
-**The two requirements look opposed and are not.** 0110 needs the set *enumerable*. The
-architecture needs it *extensible*. Those conflict only if enumerable means *written down in one
-place by hand* — which is exactly the property that let the count drift in the first place.
-
-## Considered Options
-
-1. **Keep the closed table, add each new seat by decision.** Rejected. Every capability a module
- contributes would need a change to the controller and a record before it could be offered, and
- the mesh would carry the names of services it does not itself implement.
-2. **Free-form seats, as before 0110.** Rejected for 0110's own reason, unchanged: nothing can
- say what a mesh has, and a name invented at a claim site is a name nobody can explain later.
-3. **A set that is closed at any moment and derived rather than maintained**, with the mesh's own
- seats reserved by name. Adopted. 0110 weighed options 1 and 2 and never considered this one.
-
-## Decision
-
-**A seat may be declared by a module, and the set of seats a mesh has is derived: the mesh's own,
-plus those declared by every module it has registered.** The set is still closed — a seat named
-nowhere is refused — but it is computed from the catalogue rather than written in the controller.
-
-Everything 0110 decided about what a seat *is* stands untouched: one holder at its scope; a
-definition says which seats a module *can* hold and an assignment says which it *does*; holding
-one may deliver a provision; a seat makes a role singular, never a module.
-
-**Enumeration is a query, not an inventory.** The catalogue knows every registered manifest, so
-"which seats does this mesh have, and who holds each" is answered by asking it. This is a
-stronger answer than the table gave, not a weaker one: a derived list cannot drift from reality,
-and drift is how the hand-made count came out at eleven of thirteen.
-
-**The mesh's own seats are reserved by prefix.** Every seat the mesh itself defines is named
-`mesh-*`, and a module declaring any `mesh-*` name is refused at registration. The prefix *is*
-the reservation rule — no list of reserved names to maintain, and no way for the mesh's own
-namespace to be colonised by a manifest. This requires renaming the seats that drifted from
-[ADR 0079](0079-the-foundation-seats-are-named-after-their-servers.md)'s convention: `the-catalogue`
-becomes `mesh-catalog`, `git` becomes `mesh-git`, and the node-scoped `the-build-machine`,
-`the-dns-port`, `the-intrusion-prevention`, `the-packet-filter`, `the-private-network`,
-`the-resolver-configuration`, `the-showcase` take the same prefix.
-
-The mesh's seats stay the mesh's for a reason that does not apply to a module's: **the mesh's own
-code looks them up by name.** The resolver *is* the thing that finds the store. `mesh-store` is
-not a convention the controller follows, it is an identifier the controller dereferences.
-
-**A declared seat carries a protocol.** A module declaring a seat says what may be sent to it,
-what it emits, and what it serves. The holder must satisfy it; a module may not claim a seat whose
-protocol it does not implement. Callers declare that they use the *seat*, never the module, so
-replacing the implementation changes nothing for any caller.
-
-**A seat is for a role; an event stays addressed to its emitter.** The two are not
-interchangeable and the choice is not stylistic. An event is *this happened to me* — the emitter's
-identity is the meaning, which is why the envelope carries source, node and time
-([ADR 0042](0042-the-shape-of-an-event-on-the-wire.md)); routing it through a role would erase the
-provenance an audit needs. A seat is *this capability, whoever provides it* — where not knowing
-the holder is the point. Publish an event when the fact is about you; declare a seat when you are
-offering something another module could offer instead.
-
-**Two modules declaring the same seat name is refused at registration**, second one loses.
-Registration is the last moment the mesh can still say no, and a seat name meaning two different
-protocols is the failure nobody could diagnose afterwards.
-
-## Consequences
-
-- **The controller's seat table stops being the set** and becomes the mesh's own reserved entries.
- Resolution reads the catalogue for the rest.
-- **Ten seats are renamed.** A rename is a migration, not an edit: existing assignments hold the
- old names, so the change carries a mapping and is applied once, and the lab beds that name seats
- are updated with it.
-- **A `uses` naming an undeclared seat is refused at registration**, which is where 0110's
- guarantee lands under this model — the same refusal, at the same moment, from a derived set.
-- **Adding a capability stops requiring a decision record.** That is a real loss of governance and
- the intended trade: the argument for a seat's existence moves into the module that declares it,
- where it is reviewed as part of the manifest. The mesh's own seats keep the old bar.
-- **[ADR 0041](0041-events-are-a-relationship.md)'s machinery claim is already stale** for a
- different reason, and is corrected in place there under the rule in
- [`README.md`](README.md) — a progressive insight: on JetStream a subscription is a durable
- consumer, a real object someone must create.
-- **What got harder:** a seat's protocol is now a compatibility surface between modules that do
- not know each other. Changing one breaks callers already bound to it, and nothing here says how
- that is versioned. It is the first thing to answer in the design, and the thing most likely to
- hurt later rather than now.
-
-## How it is checked
-
-- **The overview answers, and is right.** A command lists every seat, its scope, its protocol and
- its holder, derived from the catalogue — and a test asserts the count against a fixture mesh,
- because an enumeration nobody checks is how thirteen became eleven.
-- **`mesh-*` is refused to a module.** A registration test: a manifest declaring `mesh-anything`
- is refused, naming the prefix as the reason.
-- **An undeclared seat is refused.** A registration test on `uses`, and a resolution test that
- nothing reaches runtime unresolved.
-- **A second declarer loses.** A registration test: two manifests, same seat name, the second
- refused and the first untouched.
-- **A holder must satisfy the protocol.** A claim whose module does not serve what the seat
- declares is refused at assignment, not discovered when a caller times out.
-
-## Progressive insight
-
-> **Progressive insight — 2026-09-26.** *A seat rename is not a data migration.* This record's
-> consequences say "a rename is a migration, not an edit: existing assignments hold the old
-> names, so the change carries a mapping and is applied once". Implementing it showed there is
-> nothing stored to migrate: a seat's holding is **derived at resolution** from the claims in
-> manifests (`resolve.go` builds it each time), never written down, so no recorded name is left
-> pointing at the old one. What exists is source — the controller's seat table, the manifests
-> that claim them, and a manifest that may be registered later from its own repository. So the
-> change is an edit plus a **kept** rename table, which tells a manifest written against an old
-> name what it became rather than refusing it as unknown.
->
-> The decision — that modules declare seats, that the mesh reserves `mesh-*`, and that the ten
-> are renamed — is unchanged. Only the shape of the work was wrong.
-
-## References
-
-- [ADR 0110](0110-a-seat-is-a-module-assignment-from-a-closed-set.md) — superseded here; its
- requirement is kept and only its mechanism replaced.
-- [ADR 0125](0125-the-bus-is-the-only-broker.md) — one bus, which is what makes a role addressable.
-- [ADR 0079](0079-the-foundation-seats-are-named-after-their-servers.md) — the naming convention
- the reserved prefix restores.
-- [ADR 0041](0041-events-are-a-relationship.md) — the event half of the boundary drawn here.
diff --git a/02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md b/02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md
deleted file mode 100644
index e417e6e5..00000000
--- a/02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md
+++ /dev/null
@@ -1,104 +0,0 @@
----
-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.
diff --git a/02-DECISIONS/0128-the-mesh-bus-is-required-not-ambient.md b/02-DECISIONS/0128-the-mesh-bus-is-required-not-ambient.md
deleted file mode 100644
index 138b90b1..00000000
--- a/02-DECISIONS/0128-the-mesh-bus-is-required-not-ambient.md
+++ /dev/null
@@ -1,123 +0,0 @@
----
-topic: the mesh
-status: accepted
-date: 2026-09-26
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md
----
-
-# 128. The mesh bus is required, not ambient
-
-
-> **Pointer repointed, 2026-09-27.** This record was written extending
-> [ADR 0127](0127-amqp-is-a-provision-not-the-bus.md) (superseded by [ADR 0131](0131-everything-on-the-mesh-speaks-to-the-broker-seat.md)), which
-> [ADR 0131](0131-everything-on-the-mesh-speaks-to-the-broker-seat.md) has since superseded — AMQP is
-> not a provision at all. Nothing decided here changes; the frontmatter now rests on the live record,
-> and the citations below are read with that in mind.
-
-## Context
-
-[Design 29](../03-DESIGN/01-to-be/32-what-a-module-declares.md) opened by saying the bus is
-*ambient*: "No module requires it, the way no module requires a filesystem. Every module gets a
-connection and an identity whether it asks or not."
-
-**Two counts say that is wrong.** Of the 72 modules in the catalogue, **49 declare an own-secret
-named `broker` and 23 do not.** So the bus is not universal — nearly a third of the catalogue
-never speaks to it — and an ambient connection would mint an account, a password and a permission
-set for every one of those 23, each a credential nothing uses and everything must rotate.
-
-And the 49 that do take one **each hand-write the path it lands at**
-(`own-secrets: { broker: "/var/lib//broker" }`). That is a special case doing badly what
-provisioning already does well: a consumer names where a credential lands, the mesh seals it
-there, and rotation and removal follow the same path as every other credential.
-
-**The argument that made the bus ambient was narrower than it looked.**
-[ADR 0125](0125-the-bus-is-the-only-broker.md) reasoned that bus accounts cannot be provisioned
-because a provisioner is itself a module that needs an account before it can run. That is true of
-a **provisioner process**, and it is not true of a provision: the mesh's bus accounts are composed
-by the *controller*, into configuration, and the controller is not waiting on a bus account to
-exist. The circularity is real for one mechanism and absent for the other, and the earlier record
-applied it to both.
-
-## Considered Options
-
-1. **Keep the bus ambient.** Rejected on the counts above: it over-grants to 23 modules and keeps
- a hand-written path in 49.
-2. **Derive the requirement** from whether a module declares any `emits`, `consumes`, `serves` or
- `uses`. Rejected: it is the ambient model with extra inference. A reader of a manifest still
- cannot see that the module holds a bus credential, and the rule would have to be re-derived
- every time the set of bus-facing declarations grew.
-3. **The mesh bus is a provision a module requires**, delivered by the seat that holds it.
- Adopted.
-
-## Decision
-
-**A module that speaks to the mesh requires `mesh-bus`, and receives what it needs to connect.**
-The contract is an address, a credential sealed to the module, and the trust to verify the
-server. It lands where the module's manifest says, like any provision. A module that does not
-require it gets no account, no password and no permissions — and 23 modules in the catalogue
-should get none.
-
-**The `mesh-broker` seat delivers `mesh-bus`.** Its holder is the mesh's own bus, and what
-holding it delivers is the connection to that bus — which is what a seat delivering a provision
-has always meant ([design 26](../03-DESIGN/01-to-be/26-the-seats.md)).
-
-**The requirement delivers the connection; the declarations shape the authority.** They are two
-different things and both stay explicit. `requires: mesh-bus` says *this module talks to the
-mesh*; `emits`, `consumes`, `serves`, `uses` and a declared seat say *what it may say and hear*,
-and the permission set is derived from those and nothing else
-([ADR 0043](0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md)). Requiring the bus
-grants no subject; declaring a subject without requiring the bus is refused at registration as
-incoherent.
-
-**The `mesh-bus` provision is answered by the controller, not by a provisioner.** This is the
-surviving kernel of ADR 0125's bootstrap argument, narrowed to what it actually supports: the
-bus's accounts are configuration the controller composes and the server reloads
-([ADR 0106](0106-the-bus-is-nats.md) — never through a management API), so there is no provisioner
-process in the path and nothing waiting on a bus account to create bus accounts. It is a provision
-whose provider is the mesh itself.
-
-**A module may also provide a NATS server of its own, and that is a different interface.** Exactly
-as the AMQP broker provides `amqp` ([ADR 0127](0127-amqp-is-a-provision-not-the-bus.md) (superseded by [ADR 0131](0131-everything-on-the-mesh-speaks-to-the-broker-seat.md))), a module
-may run its own NATS and offer it as a backing service. That interface is **`nats`**; the mesh's
-own bus is **`mesh-bus`**; the two are never the same name, because a manifest that said `nats`
-could mean either and the difference is the whole architecture. The rule from 0119 decides which
-is legitimate: a private bus is a backing service, never a channel to another module.
-
-## Consequences
-
-- **Design 29's opening is reversed.** The bus is not ambient; it is required, and the document's
- first paragraph says the opposite of this.
-- **The seat's `Delivers` is `mesh-bus`** — corrected twice in one day, which is worth recording
- rather than tidying: it read `amqp`, which was the old broker's interface; ADR 0125 emptied it,
- on the reasoning that a bus cannot be provisioned; and it is neither. The seat delivers the
- mesh's bus.
-- **`own-secrets: { broker: ... }` is retired** in favour of the provision's own delivery, across
- 49 manifests. That is a mechanical change, and it belongs with the conversions in step 4 rather
- than step 1.
-- **23 modules lose a credential they never used.** Not a regression — an over-grant removed, and
- the smallest honest statement of what this buys.
-- **What got harder:** one more line in most manifests. The trade is that the line is true, and
- its absence is also true.
-
-## How it is checked
-
-- **A module with no `requires: mesh-bus` has no account.** A composition test: the derived user
- list contains exactly the modules that require it, and the 23 that do not appear nowhere in it.
-- **Declaring a subject without requiring the bus is refused.** A registration test on a manifest
- with `emits` and no requirement, naming the contradiction.
-- **Requiring the bus grants no subject on its own.** A composition test: a module that requires
- `mesh-bus` and declares nothing else gets a connection and an empty permission set.
-- **`nats` and `mesh-bus` are distinct interfaces.** A resolution test: a module requiring `nats`
- is answered by a module providing it, never by the seat holder, and vice versa.
-
-## References
-
-- [ADR 0125](0125-the-bus-is-the-only-broker.md) — superseded by 0119; its bootstrap argument is
- narrowed here to the case it supports.
-- [ADR 0127](0127-amqp-is-a-provision-not-the-bus.md) (superseded by [ADR 0131](0131-everything-on-the-mesh-speaks-to-the-broker-seat.md)) — a broker as a backing service; this
- applies the same shape to the mesh's own bus and separates the two names.
-- [ADR 0043](0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md) — authority from
- declarations, which this leaves untouched.
-- [design 26](../03-DESIGN/01-to-be/26-the-seats.md) — a seat delivering a provision.
diff --git a/02-DECISIONS/0129-a-seat-carries-the-protocol-of-its-role.md b/02-DECISIONS/0129-a-seat-carries-the-protocol-of-its-role.md
deleted file mode 100644
index 58ef2127..00000000
--- a/02-DECISIONS/0129-a-seat-carries-the-protocol-of-its-role.md
+++ /dev/null
@@ -1,102 +0,0 @@
----
-topic: the mesh
-status: accepted
-date: 2026-09-27
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0122-a-seat-is-data-a-rename-is-a-database-update.md
----
-
-# 129. A seat carries the protocol of its role
-
-## Context
-
-[ADR 0126](0126-a-module-declares-its-own-seats.md) let a module declare a seat with its protocol:
-what work the role accepts, what it emits, what it serves. A module's own seats work that way today.
-**The mesh's own seats — the `mesh-*` set — carry no protocol at all**, only a name, a scope and the
-provision they deliver. They say who does a job and nothing about what may be said to them or by
-them.
-
-That gap surfaced three times in one day, each time as a different-looking problem.
-
-**A build machine.** On the bus the mesh runs on today a builder has its own account kind, created by
-its own command, with permissions written by hand: read the build queue, write to two exchanges. One
-publish to a shared exchange reached all three audiences a finished build has — whoever asked, the
-controller that records it, and the catalogue that places it in the module graph. On a bus where
-permissions are per subject those are three separate grants, and nothing derives them, because a
-builder is not a module and holds a seat that promises nothing.
-
-**An event about a role rather than about a module.** The module holding the artifact-store seat
-declared an event named after a *different* module
-([issue 127](../04-ISSUES/127-a-module-event-derives-a-subject-nothing-publishes/00-report.md)). The
-bus refuses that, because a namespace belongs to who it is named for. The event is genuinely about the
-role — "the artifact store accepted an image" — and a consumer written against whichever module holds
-that role today breaks when the holder changes. There was nowhere else to put it.
-
-**A catalogue catching up.** The controller answers a request for builds it may have missed by
-re-publishing them under its own name, which no consumer of the builder's subject hears. Publishing
-them under the builder's name would be the controller signing an event as another module. Answering
-into the asker's inbox needs a grant over every inbox in the mesh, which
-[design 25](../03-DESIGN/01-to-be/25-the-bus-on-nats.md) §4 refuses.
-
-Three symptoms, one cause: **the mesh has roles it cannot describe.**
-
-## Decision
-
-**A seat carries the protocol of its role, whether the seat is a module's or the mesh's own.** The
-`mesh-*` set gains the same three fields a declared seat has — what it accepts, what it emits, what it
-serves — and the holder's authority, its work queue and its consumers are derived from them by the
-machinery that already does this for a module's seats.
-
-**Builds become work submitted to a role.** The build machine seat accepts a build and emits an
-outcome. The dedicated `mesh.build.*` branch and the stream behind it retire: a work queue shared by
-several build machines is exactly what a seat's `accepts` already is, and keeping a second mechanism
-for it means two things to reason about and two places for a permission to be wrong.
-
-**One publish still reaches three audiences, and now the mesh derived the subject.** A build's outcome
-is the seat's own event. Whoever asked matches it by the id their request carried; the controller
-records it; the catalogue places it. That is the fan-out the shared exchange gave for free, expressed
-as a subject rather than as a topology, and it means no holder needs permission to publish into
-anybody's inbox.
-
-## Alternatives considered
-
-**A dedicated principal kind for a builder**, mirroring the account the old bus issues it. Smaller: one
-addition to the composer, no change to seats, and it matches how a builder is treated today. Not taken
-because it answers one of the three symptoms and leaves the other two, and because "the builder is
-special" is a claim nobody could justify from the design — a build machine is a role the mesh has, and
-the mesh has a word for a role.
-
-**Leaving the outcome as a reply to the asker's inbox.** Rejected on authority: a holder able to answer
-any asker needs a grant across the whole inbox space, which is the one grant design 25 §4 refuses by
-name. The seat's event costs the asker a filter and costs the mesh nothing.
-
-## Reconciled with 0122, which landed in parallel
-
-*Added 2026-09-27, on merging.* [ADR 0122](0122-a-seat-is-data-a-rename-is-a-database-update.md) moved
-the seat set out of compiled code and into a table the controller owns. This record was written against
-the slice, and says the `mesh-*` set "gains the same three fields a declared seat has".
-
-**The decision is unaffected and the mechanism is better for it.** What a seat accepts, emits and serves
-becomes three columns beside its name and scope, so giving a role a protocol is a write rather than a
-rebuild — which is the whole argument of 0122 applied to the thing this record adds. Where this text
-says the set gains fields, read: the table gains columns.
-
-## Consequences
-
-**A seat is now the mesh's unit of "a role that talks".** A role that accepts work, announces outcomes
-or answers questions says so where it is defined, and everything about permissions, queues and
-consumers follows. Nothing hand-writes a grant for a role again.
-
-**The shared library cannot yet publish on a seat, and that is now the blocking gap rather than a
-curiosity.** A module holding a seat has the authority and no way to use it; the build machine is
-written in Go and reaches the bus directly, so it is unaffected, but the artifact-store event stays
-under its module's own name until the library has a surface for this. That is a task, and this record
-is what makes it one.
-
-**A second mechanism disappears.** `mesh.build.*`, the BUILDS stream and the builder's hand-written
-account all retire. Fewer things, and the ones left are derived.
-
-**The catch-up question is not settled by this**, only made answerable: a seat that serves something
-gives the controller a way to be asked, which the mesh did not have. Whether catch-up should be a
-question at all remains open.
diff --git a/02-DECISIONS/0130-the-predecessor-is-ending-and-its-broker-goes-with-it.md b/02-DECISIONS/0130-the-predecessor-is-ending-and-its-broker-goes-with-it.md
deleted file mode 100644
index 32e3a37a..00000000
--- a/02-DECISIONS/0130-the-predecessor-is-ending-and-its-broker-goes-with-it.md
+++ /dev/null
@@ -1,71 +0,0 @@
----
-topic: the mesh
-status: accepted
-date: 2026-09-27
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md
----
-
-# 130. The predecessor is ending, and its broker goes with it
-
-
-> **Pointer repointed, 2026-09-27.** This record was written extending
-> [ADR 0127](0127-amqp-is-a-provision-not-the-bus.md) (superseded by [ADR 0131](0131-everything-on-the-mesh-speaks-to-the-broker-seat.md)), which
-> [ADR 0131](0131-everything-on-the-mesh-speaks-to-the-broker-seat.md) has since superseded — AMQP is
-> not a provision at all. Nothing decided here changes; the frontmatter now rests on the live record,
-> and the citations below are read with that in mind.
-
-## Context
-
-[ADR 0127](0127-amqp-is-a-provision-not-the-bus.md) (superseded by [ADR 0131](0131-everything-on-the-mesh-speaks-to-the-broker-seat.md)) settled that the old broker is an ordinary
-provider of the `amqp` provision rather than a compatibility module with an end date. It rejected
-giving it a retirement condition, and said why: *"its clients are not only the predecessor's, so the
-retirement condition describes a day that will not come."*
-
-**The operator has said that day is coming.** The predecessor is deprecated. Some of it is still
-running, and it is not being migrated — it is being left to stop. Its broker may be shut down.
-
-That is a fact about this installation, not a change of mind about what a broker is. It is recorded
-because three documents reason from the premise it overturns:
-[design 25](../03-DESIGN/01-to-be/25-the-bus-on-nats.md) §5 and §9, and
-[design 28](../03-DESIGN/01-to-be/28-building-the-bus.md)'s closing note that the predecessor's world
-"does not need to move: its broker is the compatibility module until its last client is gone."
-
-## Decision
-
-**The predecessor's broker retires when nothing requires `amqp`, by being unassigned like any other
-provider.** No retirement condition, no end-date machinery, no special case — which is ADR 0127 being
-paid off rather than revised. Because that record made the broker an ordinary provider, ending it
-needs nothing that does not already exist: a provision with no consumers has its provider unassigned,
-and the module system has done that since it existed.
-
-**So step 5.3 has an ending.** "The mesh's own accounts removed from the deprecated broker" was
-written as the last thing that could be said, because the broker itself was going to outlive the
-question. It now finishes: once the mesh's own traffic has moved and the predecessor's remnants have
-stopped, the module is unassigned and the port is free.
-
-**And the transitional doubling has a date.** The build outcome is announced under both the module's
-name and the role's on the old bus, so that a catalogue deployed before the rename and one deployed
-after both hear it. That exists only while the old bus does, and goes with it.
-
-## Consequences
-
-**The remote access path goes with it, and that is the one practical consequence worth planning
-around.** The predecessor's own mesh communicates over that broker — so shutting it down ends the
-tooling that reaches this installation's machines remotely. Work on the node after that point is done
-from the node. **This matters most for the rollout**, which is the step that would otherwise be driven
-from a workstation: it has to be driven locally, or driven before the broker stops.
-
-**What is still running on it stops when it stops.** Some of the predecessor's services are live and
-are not being moved. That is the operator's decision and it is recorded here so that nobody later reads
-a broker with clients as an accident.
-
-**Nothing in a served request's path is affected.** Modules serve from their own containers; the mesh's
-bus carries the mesh's own traffic — declarations, reports, events, tool calls. This was checked rather
-than assumed when the question came up, and it is why the operator's position (*"as long as my services
-keep running"*) is a bounded risk rather than a gamble.
-
-**One reason to keep the broker survives**: `amqp` remains a provision a module may require, and a
-module that genuinely needs an AMQP broker can be given one. What retires is *this* broker's role as
-the predecessor's, not the mesh's ability to provide the thing.
diff --git a/02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md b/02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md
deleted file mode 100644
index 5b1155b7..00000000
--- a/02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md
+++ /dev/null
@@ -1,94 +0,0 @@
----
-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 |
diff --git a/02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md b/02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md
deleted file mode 100644
index 3400298a..00000000
--- a/02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md
+++ /dev/null
@@ -1,150 +0,0 @@
----
-topic: the mesh
-status: accepted
-date: 2026-09-28
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0129-a-seat-carries-the-protocol-of-its-role.md
----
-
-# 132. A seat carries the tools its holder must serve
-
-## Context
-
-[ADR 0129](0129-a-seat-carries-the-protocol-of-its-role.md) gave a seat the protocol of its role in
-three parts: the work it accepts, the events it emits, and the verbs it **serves** — request and
-reply, awaited. The bus already derives authority from all three: a holder subscribes
-`mesh.seat..tool.`, and a module that uses the seat may publish it and nothing else.
-
-**The serving third has never been used.** The mesh defines 14 seats, 8 mesh-scoped and 6
-node-scoped. Exactly one carries a protocol at all — the build machine, which accepts `build` and
-emits `built`. Not one seat declares a single verb it serves. The mechanism is built, enforced, and
-empty.
-
-Meanwhile every tool on the mesh is addressed to a module. Of 72 modules in the catalogue, 45 serve
-tools, about 203 of them, each on `mesh.mod..tool.`. So a caller binds to the module
-that happens to hold a role rather than to the role, and replacing that module breaks every caller —
-which is the thing seats exist to prevent everywhere else.
-
-**Nothing can say what tools exist.** Measured on 2026-09-28, with the bus carrying the whole mesh: a
-workstation client holding an operator credential connected, the bus accepted the account, and
-`mesh call gitea.gitea_list_repos` answered with real repositories. The same client's `mesh tools`
-found nothing, because it asks `mesh-catalog.catalog_tools` and no module serves that: the catalogue
-serves `catalog_modules`, `catalog_module`, `catalog_provides`, `catalog_dependents` and
-`catalog_stale`. An agent can therefore call any tool it already knows the name of and discover none.
-MCP's `tools/list` is that same question, so the MCP surface is a working transport over an empty
-catalogue.
-
-**And there is nowhere for a tool's definition to live.** A manifest has a `tools` field: 0 of the 45
-modules that serve tools fill it. That is not neglect, it is the arrangement failing — the field was
-the bus grant's source for what a module may subscribe, and because nothing filled it every module
-that served a tool was refused its own subscription on the new bus, live, until the grant was changed
-to the module's own namespace. Today a tool's name, description and argument schema exist only in the
-module's code.
-
-Two facts about the machinery matter for what follows. A seat's protocol is not in the store: the seat
-rows lack the ADR 0129 columns, so the protocol comes from compiled defaults and is merged in when a
-row is read. And `seatSubject` is flat — `mesh.seat...` with no node in it — so a
-node-scoped seat's tool call would reach every node's holder at once, and the holders' queue group
-would hand it to whichever answered first.
-
-## Decision
-
-**A seat's protocol carries its tools in full**: the verb, what it does, and the schema of its
-arguments and of its answer. The seat is the definition of the role's interface; the holder is an
-implementation of it.
-
-**Serving the seat's tools is a condition of holding the seat.** A module that does not serve every
-verb the seat declares may not occupy it. This is checked where the other conditions of holding are
-checked — registration and handover — and refused by naming the verbs that are missing.
-
-**A role's tools are addressed to the role.** `mesh.seat..tool.` mesh-wide. A node-scoped
-seat carries the node in the address, because one subject reaching six machines' holders is not an
-address, and the queue group that made it look like one would silently pick a winner.
-
-**A module keeps its own tools, and both exist.** `gitea_list_repos` stays, because gitea can run
-without holding the `git` seat — a second forge, an instance kept for one purpose. The module's name
-answers *this gitea*; the seat's verb answers *whoever is the forge*. Which of the two a caller wants
-is a decision in the running session, not one the mesh makes for it.
-
-**What answers "what tools exist" follows where the definition lives.** A seat's tools are read from
-the mesh's own records. A module's own tools are answered by the module, from the code that defines
-them. Discovery is therefore a read for the durable half and a question to the running mesh for the
-free half.
-
-**A seat's tools are an interface, and change like one.** Additive within a version; a change that
-would break a caller takes the version token the subject already has room for (design 29 §8), and the
-two run side by side until nothing is bound to the old one.
-
-**The mesh's own verbs are the `mesh-controller` seat's tools.** `status`, `push`, `build`, `assign`
-and the rest are a role's interface, not a container's, and the audit point [ADR 0095](0095-the-control-plane-is-the-way-to-ask-a-module.md)
-asks for is the seat's holder.
-
-## Options considered
-
-1. **The manifest declares each module's tools.** Rejected. The list is then written twice — in the
- manifest and in the code — and a schema in a manifest goes stale silently, which is the worst kind
- of wrong for something an agent reads to decide what to call. It is also the arrangement that has
- already failed once: the field exists, 0 of 45 modules fill it, and the grant that depended on it
- refused every tool subscription on the mesh.
-2. **Every runtime answers an introspection call, and something aggregates them.** Rejected as the
- shape for a role's tools, kept for a module's own. An aggregator needs permission to publish into
- every module's namespace, which is a widening the mesh otherwise gives only to the control plane;
- and the answer is only as available as the modules are, so a mesh whose catalogue cannot say what a
- role answers while its holder is down cannot plan against it.
-3. **The control plane answers everything.** Rejected. It puts a tool surface on the control plane for
- tools it does not implement, and makes discovery depend on the one component that must stay
- answerable while it is itself being replaced. The mesh's own verbs are its to answer, and it answers
- them as the holder of a seat.
-4. **Seats only; no module tools.** Rejected. Most modules hold no seat, and inventing a seat per
- module to give its tools a home would dilute what a seat is: one holder of a role the mesh needs
- exactly one of.
-
-## Consequences
-
-**One capability can have two names, deliberately.** A forge that holds the `git` seat answers both
-`mesh.seat.git.tool.list_repos` and `mesh.mod.gitea.tool.gitea_list_repos`. This is the one place the
-mesh accepts two names for one thing, because they are answers to different questions and the second
-one survives the module not holding the seat. The glossary rule stands everywhere else.
-
-**A seat becomes a contract to implement.** Adding a verb to a seat is a change every holder must
-make, and a claim that was valid becomes invalid until it does. That is the point, and it is also the
-reason a seat's tools should be few and durable while a module's own stay free.
-
-**Three prerequisites, none of them in place.** The seat's protocol must be in the store rather than in
-compiled defaults, or discovery reads a binary rather than the mesh. The protocol must become richer
-than a list of verbs, because a verb without a schema is not something an agent can call. And a
-node-scoped seat needs the node in its subject before any of its tools can exist.
-
-**Discovery becomes cheap for the half that matters.** What roles the mesh has and what each answers is
-a query, with no fan-out and nothing to be up. An agent's authority can then be role-shaped — *the
-forge's tools* — rather than a list of module-specific names that changes when a module is replaced.
-
-**The MCP surface belongs inside the mesh.** Once the tools are the mesh's own records, the thing that
-serves them to an agent is a module the mesh assigns to the machine where the agent sits, with a
-credential the mesh minted and authority derived from what it may call — not a program started by hand
-with a credential printed to a terminal.
-
-## How this is checked
-
-- **Holding is refused without the verbs.** The condition sits with the other conditions of holding a
- seat, so registration and a handover both refuse a module that does not serve what the seat declares,
- and the refusal names the missing verbs. A test per condition, as the other seat conditions have.
-- **The grant is derived from the seat, and already is.** A holder's subscription and a user's publish
- come from the seat's protocol, so a verb nobody declared is a subject nobody may use, and a verb the
- seat declares reaches exactly its holder. The golden composition of the bus's user list is the test
- that keeps it honest.
-- **Discovery is a read, and is tested as one.** What the mesh answers for a seat's tools equals what
- the seat's records declare — no call to a module in the path, so the test needs no running module.
-- **A node-scoped seat's subject carries its node**, checked by the same test that checks the subject
- table: two nodes holding one node-scoped seat derive two addresses.
-
-## References
-
-- [ADR 0129](0129-a-seat-carries-the-protocol-of-its-role.md) — the protocol this widens
-- [ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md) — the role, its work and its events
-- [ADR 0095](0095-the-control-plane-is-the-way-to-ask-a-module.md) — a tool call passes one process where an audit belongs
-- [ADR 0126](0126-a-module-declares-its-own-seats.md) — an event is addressed to its emitter, for the same reason a role's verb is addressed to its role
-- [`03-DESIGN/01-to-be/26-the-seats.md`](../03-DESIGN/01-to-be/26-the-seats.md) — how a seat is held and handed over
-- mesh-controller #116, #117, #118 — the grants as they now stand: a module serves its own namespace, the control plane may ask any tool
-- Measured 2026-09-28 on the live mesh: an operator credential calling a module's tool over the bus answers; `tools/list` finds nothing
diff --git a/02-DECISIONS/0133-a-module-owns-its-migrations-and-the-mesh-owns-when-they-run.md b/02-DECISIONS/0133-a-module-owns-its-migrations-and-the-mesh-owns-when-they-run.md
deleted file mode 100644
index ab77e0b8..00000000
--- a/02-DECISIONS/0133-a-module-owns-its-migrations-and-the-mesh-owns-when-they-run.md
+++ /dev/null
@@ -1,159 +0,0 @@
----
-topic: what runs on it
-status: superseded
-date: 2026-09-28
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0052-a-step-that-runs-once-before-a-container.md
-superseded-by: 02-DECISIONS/0135-a-module-version-prepares-its-state-before-it-runs.md
----
-
-# 133. A module owns its migrations, and the mesh owns when they run
-
-## Context
-
-On 2026-09-28 the mesh replaced its own control plane, through its own upgrade path, with a build
-carrying a migration. Nothing applied it. For the next three quarters of an hour every build the mesh
-made was refused by the store with one line — *column "built_contexts" does not exist* — which reached
-only whoever happened to be waiting on that build's reply. The images were built and published, so the
-registry filled with artifacts the mesh has no record of, and the overview went on reporting that every
-module was current ([issue 133](../04-ISSUES/133-the-control-planes-schema-is-migrated-at-birth-and-never-again/00-report.md)).
-
-The schema had been created once, at genesis, by an action in the foundation bundle. Nothing ran it
-again, through many updates of the control plane since.
-
-**The mechanism to do this right already existed and one module used it wrong.**
-[ADR 0052](0052-a-step-that-runs-once-before-a-container.md) makes a run-once container a step the host
-runs to completion before whatever the declaration places after it, and names migrating a schema as the
-case it exists for. Three facts about how it is used today:
-
-- The control plane's manifest had no step at all. The immediate fix was to write one by hand, and that
- hand-written step repeats three environment variables and three volume mounts from the server
- resource it precedes — six chances to drift from the thing it prepares.
-- Two other modules hand-write the same shape for the same reason: gitea's admin bootstrap and
- mosquitto's dynsec seed, each repeating its sibling's image, environment and mounts. One of them
- ends in `|| true`, which is a lock implemented as a shrug.
-- The catalogue module takes the other road: it migrates its own schema in its own code when it starts.
- That failure mode is a crash loop rather than a stop — the catalogue restarted 338 times this
- morning on an unrelated start-time failure, and nothing anywhere said the mesh's graph had a gap.
-
-**What the mesh already has, and what HAL needed stages for.** Ordering a provider before its consumer
-is `providersFirst`, which topologically orders a node's modules. Ordering within a module is
-declaration order, and a run-once container gates everything after it. Remembering that a step has
-already run is the digest of its declaration, recorded only after it exits 0
-([ADR 0018](0018-a-picture-is-read-from-what-runs.md)) — and the image is part of that digest, so a new
-build re-runs it. Three of the four things a stage system provides are therefore already here. The
-fourth — that a module has a schema at all — is the only thing missing.
-
-**Nothing in the catalogue ships a migrations directory.** Of 72 modules, none has one; the modules that
-migrate do it in their own code. So this is not a decision about where SQL files live. It is a decision
-about who runs them and when.
-
-**Two facts bound what is safely expressible.** A node converges toward its own declaration without
-waiting on any other node. And of the five modules that run on more than one machine today — dnsmasq,
-fail2ban, networking, networkmanager, sshd — not one wants a store; every module with a database is on
-exactly one machine.
-
-## Decision
-
-**A container may declare steps to run before it.** The same container, run to completion, with
-different arguments, in order, before it starts. The mesh derives the run-once resources from that
-declaration, so the image, the environment, the volumes, the network and the credentials come from the
-one place they are already described and cannot drift from it.
-
-**A module's migrations are the first user of this, and the module owns them entirely.** The SQL, the
-order, the idempotence, the lock, and which dialect it speaks. The mesh never learns that postgres and
-mssql differ, because it runs the module's own image with the module's own arguments against the
-module's own binding and requires exit 0. A module needing both stores runs one step that does both.
-
-**The mesh owns the moment, and the gate is the guarantee.** Whether a version may serve when its
-schema is not there yet is a deployment question, and the mesh is the only thing that can answer it,
-because the mesh is what starts the container. A step that fails stops the container it precedes, so
-a failed migration is a version that does not serve rather than a version serving against a store it
-does not match.
-
-**Per node, and there is no level.** The step runs wherever the module runs. A step that ran "once,
-somewhere" would leave every other machine with no gate at all, and additive migrations protect old
-code against a new schema, never new code against an old one. The cost is an obligation a migration
-runner already carries: a version table and a lock.
-
-**"Once, mesh-wide" is what holding a seat means.** A step that is not idempotent — seeding an
-account, sending a notice, taking a backup — belongs to a module that holds a seat, where the mesh
-already guarantees one holder, on record, handed over deliberately. That is the answer to the level
-question rather than a field that has to invent an election and keep it somewhere.
-
-**Migrations are forward-only and additive.** The step runs before the *new* container starts, so the
-old one is still running against the new schema for the length of the apply.
-
-**Declared, never inferred.** The control plane cannot see inside an image, so a module that ships
-migrations and declares no step is not refusable at registration; it breaks on its first upgrade. This
-record says so rather than implying a check that cannot exist.
-
-## Options considered
-
-1. **Each module migrates itself when it starts** — what the catalogue does today. Rejected: it turns a
- schema failure into a crash loop instead of a stop, it is invisible in the declaration so nothing can
- say the module even has a schema, and two machines running the module both migrate at start with
- nothing sequencing them.
-2. **The mesh applies migrations itself**, with a driver and a version table per store — HAL's shape.
- Rejected: the mesh would have to know one store type from another, hold another module's store
- credentials, and reach a machine to use them, which [ADR 0005](0005-the-node-host.md) forbids. It is
- also the reason that shape needs levels: something central has to decide where the once happens.
-3. **A hook lifecycle** — pre-build, post-build, pre-deploy, post-deploy. Rejected: there is no deploy
- event here to hook. A declaration is a desired state applied in order and reconciled forever, so
- "pre-deploy" is exactly "a step before this container", pre- and post-build are what a Dockerfile and
- the artifact list already are, and "post-deploy" has no moment to name.
-4. **A hook level** — once per module, or once per module-node assignment. Rejected as a field, kept as
- a property: see the decision. A once-per-module step needs cross-node ordering underneath it to be
- safe, and a node converging without waiting on its neighbours is worth losing on purpose rather than
- by accident.
-5. **Every module hand-writes its own run-once step** — the immediate fix for the control plane.
- Rejected as the general answer: it duplicates the resource it precedes, in three places already, and
- a hand-written step is one the next module forgets. Forgetting it is the fault this record exists
- for.
-6. **Record a schema level per module in the store.** Rejected: gating makes the invariant true by
- construction, so a level is a second account of the same fact and the first one to go stale.
-
-## Consequences
-
-**Three hand-written steps collapse into one line each**, and the control plane's own migrate step stops
-repeating its server's environment and mounts.
-
-**The catalogue's self-migration becomes the exception to remove.** One shape, and the mesh's own
-control plane is not an exception to it either.
-
-**A module on two machines with one shared store must lock.** Today none is, so this is an obligation
-stated before it is needed rather than discovered by two concurrent migrations.
-
-**There is still no readiness-gated step.** Only an action carries `verify`; a container has no health
-notion, so "run this once the service answers" remains unexpressible and seeding through a running
-service's API has no home. That is its own decision about a container's readiness, and this record does
-not make it.
-
-**Genesis keeps its own action.** At birth there is no control plane to derive anything from, which is
-what [ADR 0067](0067-genesis-is-a-pivot.md) already says about that moment.
-
-## How this is checked
-
-- **The composition carries the step.** A test on a node's composed declaration: every container that
- declares steps before it is preceded by them, and the derived step's image, environment, volumes and
- network equal the container's — so the two cannot drift, which is the failure the hand-written kind
- has.
-- **A failed step stops what follows.** The host already refuses to go on past a run-once step that did
- not exit 0; the test for that is extended to a derived one, so the gate is checked rather than
- assumed.
-- **The mesh's own schema is covered by the same mechanism as everything else.** The control plane
- declares its step in its own manifest, so the case that failed on 2026-09-28 is the case the test
- covers.
-- **A module claiming a seat for a once-only step is checked where seats are checked** — the conditions
- of holding, not a new mechanism.
-
-## References
-
-- [ADR 0052](0052-a-step-that-runs-once-before-a-container.md) — the step this extends
-- [ADR 0018](0018-a-picture-is-read-from-what-runs.md) — a digest is the record that something happened
-- [ADR 0005](0005-the-node-host.md) — the control plane decides and never touches a machine
-- [ADR 0067](0067-genesis-is-a-pivot.md) — why genesis does it differently, once
-- [issue 133](../04-ISSUES/133-the-control-planes-schema-is-migrated-at-birth-and-never-again/00-report.md) — the failure that produced this record
-- [`03-DESIGN/01-to-be/32-what-a-module-declares.md`](../03-DESIGN/01-to-be/32-what-a-module-declares.md) §6 — the lifecycle this sits in
-- Measured 2026-09-28: three hand-written run-once steps repeating their sibling's resource; 0 of 72 modules with a migrations directory; 5 modules on more than one machine, none of them wanting a store
diff --git a/02-DECISIONS/0134-the-mesh-says-what-it-applied.md b/02-DECISIONS/0134-the-mesh-says-what-it-applied.md
deleted file mode 100644
index 6e325b46..00000000
--- a/02-DECISIONS/0134-the-mesh-says-what-it-applied.md
+++ /dev/null
@@ -1,126 +0,0 @@
----
-topic: the mesh
-status: accepted
-date: 2026-09-28
-deciders: jochen
-reconstructed: false
----
-
-# 134. The mesh says what it applied
-
-## Context
-
-The pipeline is observable on the bus from a merge to an artifact, and modules already plug into it:
-the forge emits `pull.merged`, the build machine's seat emits `built`, the catalogue emits `registered`,
-`upgraded` and `rebuild-needed`, providers emit `postgres.database.provisioned` and its siblings. Things
-consume them today — the catalogue consumes `built`, each provider consumes its own provisioning events,
-model-usage consumes `*.usage.*`, the audit logger consumes `**`. Nothing had to be invented for any of
-that; subscribing *is* plugging in.
-
-**It goes dark at the moment it touches a machine.** A host applies a declaration and reports to the
-control plane on the control branch, which only the control plane may read — correctly, because a report
-carries what a machine is and enrolment travels the same way. So nothing on the mesh says *this machine
-now runs version Y of module Z*, or that it refused to, or why.
-
-What that cost on 2026-09-28, in one morning:
-
-- A build result the store refused was visible only to whoever was waiting on that build's reply. For
- three quarters of an hour the mesh built things and recorded none of them, while the overview said
- every module was current ([issue 133](../04-ISSUES/133-the-control-planes-schema-is-migrated-at-birth-and-never-again/00-report.md)).
-- A module crash-looping at start — 338 restarts — was found by reading a container's logs by hand.
- Nothing on the bus said the mesh's graph had stopped learning.
-- A run-once step that fails now stops an upgrade by design ([ADR 0133](0133-a-module-owns-its-migrations-and-the-mesh-owns-when-they-run.md)),
- and the same silence would cover it: the version simply would not appear.
-
-**And the one thing the control plane does emit is refused by its own account.** Answering a catalogue
-that asks to catch up, it publishes each recorded build under `mesh.mod.control-plane.event.…` — a
-module namespace for a module that does not exist. Its own permissions refuse it, so a catalogue that
-restarts gets nothing and keeps its gap. The control plane has facts to state and nowhere to state
-them.
-
-## Decision
-
-**The mesh emits the deploy half of the pipeline as facts on the bus.** What a machine now runs, and
-what it refused to run and why. Both are facts about the mesh doing its work, in the same form as every
-other fact on the bus, so anything that wants them subscribes the way the catalogue subscribes to
-`built`.
-
-**The control plane states them, as the holder of the `mesh-controller` seat.** Its facts live under the
-seat's own namespace, which is where a role's events belong
-([ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md),
-[ADR 0129](0129-a-seat-carries-the-protocol-of-its-role.md)) and which survives the control plane being
-replaced. That is also what gives the catch-up replay a subject it may publish instead of an invented
-module namespace.
-
-**Emitted when what a machine runs changes, not on every convergence pass.** A host reconciles
-continuously and reports each time; a fact per pass would be a fact per minute per machine that says
-nothing. The report carries the declaration it applied and what changed, so the control plane has what
-it needs to speak only when there is something to say.
-
-**A refusal is a fact with a subject in it** — which machine, which resource, and the reason as the host
-gave it. A refusal that names only the machine is the silence this record is about, one level up.
-
-**Reports stay where they are.** A node's report remains control traffic that only the control plane
-reads. The deploy facts are derived from it, which makes them second-hand on purpose: one emitter, one
-ordering, and no widening of the narrowest account in the mesh.
-
-## Options considered
-
-1. **Leave it as it is, and let whatever cares ask the control plane.** Rejected: asking for a fact that
- already arrives is the shape the mesh removed everywhere else, and nothing can react at the moment a
- machine changes — which is exactly when a graph, an audit or an operator wants to know.
-2. **Each node emits its own facts.** Rejected: it widens every host's account to an event namespace, and
- a host's authority is deliberately the narrowest in the mesh. Its report already reaches the one thing
- that can speak for it.
-3. **Widen who may read the control branch.** Rejected: that branch carries what machines say *to* the
- control plane, enrolment included. Widening its readers widens that too, for an unrelated reason.
-4. **A registry of deploy hooks** — something registers interest and is called. Rejected: an event is
- already the mechanism; there is nothing to register, and a callback is an address the mesh spent
- [issue 102](../04-ISSUES/102-an-address-recorded-at-genesis-or-build-does-not-follow-the-nodes-ports/00-report.md)
- learning not to keep.
-5. **Put the facts on the node's own declaration stream.** Rejected: that stream is last-per-subject by
- design, so a machine away for an hour gets exactly the current declaration and nothing older. A
- history of what happened cannot live in a stream built to forget.
-
-## Consequences
-
-**The audit logger gets the deploy half for nothing**, because it consumes everything.
-
-**A failure becomes visible where the mesh is watched** rather than where someone happened to be
-looking. That answers the open question [issue 133](../04-ISSUES/133-the-control-planes-schema-is-migrated-at-birth-and-never-again/00-report.md)
-left about a record the store refused.
-
-**The catch-up replay stops being a burst of events.** With the control plane able to state its own
-facts, replaying history as if it were happening now is a choice rather than the only option — and the
-better shape is the question the catalogue is actually asking, answered once
-([design 33](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md)).
-
-**The facts are second-hand.** The control plane says what a machine reported, so a machine that cannot
-reach the bus produces no fact at all. Absence is not health, and what a machine was last heard from
-stays the place that says so.
-
-**The events stream carries more.** Bounded by emitting on change rather than on every pass, and each
-fact is small; the stream's own limits remain what keeps it finite.
-
-## How this is checked
-
-- **The control plane's grant names exactly the subjects it emits**, derived from its seat like every
- other principal's, and the composed user list is compared against a golden file — so a fact it cannot
- publish fails a test rather than a catalogue's replay.
-- **A convergence that changed nothing emits nothing.** A test with two identical reports and one
- expected fact, because the failure this guards against is a fact per minute per machine.
-- **A refusal names its resource.** A test where a host reports a failed resource and the emitted fact
- carries which one and why, not merely that something went wrong.
-- **What the mesh emits is what something consumes.** The subject a module declares it consumes derives
- to the subject the control plane publishes — the same agreement test that already keeps the
- controller's own subscriptions honest.
-
-## References
-
-- [ADR 0041](0041-events-are-a-relationship.md) — an event is a relationship, not a call
-- [ADR 0126](0126-a-module-declares-its-own-seats.md) — an event is addressed to its emitter, because the emitter's identity is the meaning
-- [ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md), [ADR 0129](0129-a-seat-carries-the-protocol-of-its-role.md) — a role's events belong to the role
-- [ADR 0083](0083-one-push-leaves-the-mesh-consistent.md) — a report is held for the store rather than lost
-- [ADR 0133](0133-a-module-owns-its-migrations-and-the-mesh-owns-when-they-run.md) — the gate whose failure this makes visible
-- [`03-DESIGN/01-to-be/32-what-a-module-declares.md`](../03-DESIGN/01-to-be/32-what-a-module-declares.md) §6 — the lifecycle, which ends today at a report nobody else may read
-- Measured 2026-09-28: 45 minutes of builds recorded nowhere with the overview reporting health; a module at 338 restarts found by hand; the control plane's only emitted event refused by its own permissions
diff --git a/02-DECISIONS/0135-a-module-version-prepares-its-state-before-it-runs.md b/02-DECISIONS/0135-a-module-version-prepares-its-state-before-it-runs.md
deleted file mode 100644
index e63b15e6..00000000
--- a/02-DECISIONS/0135-a-module-version-prepares-its-state-before-it-runs.md
+++ /dev/null
@@ -1,153 +0,0 @@
----
-topic: what runs on it
-status: accepted
-date: 2026-09-28
-deciders: jochen
-reconstructed: false
-supersedes: 0133-a-module-owns-its-migrations-and-the-mesh-owns-when-they-run.md
----
-
-# 135. A module version prepares its state before it runs
-
-## Context
-
-[ADR 0133](0133-a-module-owns-its-migrations-and-the-mesh-owns-when-they-run.md) settled who runs a
-module's migrations and when, and it said so in the wrong vocabulary. It put the declaration on a
-*container* — "a container may declare steps to run before it" — and derived the scope of the work from
-the *machine*. Both are wrong at the level a module author works at, and the second is wrong on the
-facts.
-
-**A container is one resource kind the host applies.** A module has code, state and a version; whether
-its artifact is an image, a bundle or something later is the mesh's business. The module-facing
-vocabulary for a module's own code already exists and has nothing to do with a container runtime: a
-module declares **entrypoints** — this file is my tools, this file is my provisioner — and the mesh runs
-them. A manifest that says "run this container with these arguments, and here are the volumes and
-environment again" has an author writing down the machine's business twice.
-
-**And the scope is not the machine's to decide, because the mesh already decided what a state is.** A
-consumer is a module *on a machine* (migration 0015, from
-[issue 022](../04-ISSUES/022-one-credential-per-node-per-provision-not-per-module/00-report.md)):
-the mesh derives a login per consumer and the provider creates a database owned by exactly that login
-([ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md)). So a module on three machines is three
-consumers, three credentials and three databases. There is no shared state for two machines to race over,
-and ADR 0133's central caveat — that a module's migrations must take a lock because two machines might
-migrate at once — describes a situation the mesh does not currently produce.
-
-That correction makes the whole "level" question HAL answered with stages disappear: the scope of
-preparation is the scope of the state, and the mesh knows it.
-
-What the earlier record got right and this one keeps: the module owns the work, the mesh owns the moment,
-the gate is the guarantee, migrations stay forward-only, and none of it can be inferred from inside an
-artifact. What produced it also stands — the control plane was replaced with a build carrying a migration,
-nothing applied it, and for three quarters of an hour every build was refused by the store with one line
-that reached only whoever was waiting on a reply
-([issue 133](../04-ISSUES/133-the-control-planes-schema-is-migrated-at-birth-and-never-again/00-report.md)).
-
-## Decision
-
-**A module version declares an entrypoint that prepares its state.** One name in the manifest, in the
-same vocabulary as the entrypoints it already declares for its tools and its provisioner. No container,
-no command line, no environment, no mounts — those are how a machine runs the module's code, and the
-module already said that once.
-
-**The mesh runs it as it runs that module's own code, to completion, in the module's own context.** Every
-binding, credential and setting the module's code would receive, because it *is* the module's code. How a
-machine does that is the host's business and stays there: for an image artifact it is the step
-[ADR 0052](0052-a-step-that-runs-once-before-a-container.md) already defines, and a later kind of artifact
-changes the host, not the manifest.
-
-**Preparation gates the version.** A version whose preparation did not succeed does not run — anywhere.
-Since the rollout already sends machines one at a time and stops at the first that does not take a
-version, a preparation that fails stops the rollout there, leaving every other machine on the version
-that works.
-
-**Preparation is scoped to the state, and the mesh derives that scope.** State the mesh provisions is per
-consumer — a module on a machine — so preparation happens once per consumer. State the module keeps on
-the machine is per machine, which is the same answer. A module that holds an exclusive seat has one of
-itself, so its preparation happens once by definition. No level, no election, no cross-node ordering, and
-no lock obligation invented for a race the mesh does not create.
-
-**Once per version per state.** A version bump attempts preparation once against each state it has; the
-module's own runner decides there is nothing to do, which is what a runner with a version table does
-anyway. A retry after a partial failure runs it again, so the work is the module's to make safe against
-that — the one obligation no design can remove.
-
-**Forward-only and additive.** Preparation runs while the previous version is still serving, so a
-migration that removes or renames what the old code reads breaks the mesh in the window between the two.
-
-**Declared, never inferred.** The control plane cannot see inside an artifact, so a module that ships
-migrations and declares no entrypoint is not refusable at registration. It breaks on its first upgrade,
-and this record says so rather than implying a check that cannot exist.
-
-## Options considered
-
-1. **A container declares steps before it** — [ADR 0133](0133-a-module-owns-its-migrations-and-the-mesh-owns-when-they-run.md).
- Superseded, not because the mechanism is wrong but because the *declaration* is in the wrong place: it
- makes every module author restate the machine's arrangement, and it ties a module's own lifecycle to
- one resource kind. The host-side mechanism it named is retained and is now an implementation detail.
-2. **Each module prepares itself when it starts** — what the catalogue does today. Rejected: a schema
- failure becomes a crash loop rather than a stop, nothing in the declaration says the module has a
- state to prepare, and the version serves the moment it starts rather than after the state is right.
-3. **The mesh applies migrations itself**, with a driver and a version table per store type. Rejected:
- the mesh would have to know one store from another, hold another module's credentials and reach a
- machine with them, which [ADR 0005](0005-the-node-host.md) forbids. It is also what forces a stage
- system: something central has to decide where the work happens.
-4. **A hook lifecycle** — pre-build, post-build, pre-deploy, post-deploy. Rejected: a declaration is a
- desired state reconciled forever, so there is no deploy moment to hook. "Pre-deploy" is exactly this
- record; pre- and post-build are what a recipe and the artifact list already are; "post-deploy" names
- nothing that happens.
-5. **A declared level** — once per module, or once per assignment. Rejected: the mesh already knows what a
- state is, so asking an author to choose is asking them to restate a fact the mesh holds, with a chance
- of contradicting it.
-6. **Record a preparation level per module in the store.** Rejected for the reason ADR 0133 gave and this
- record keeps: gating makes the invariant true by construction, and a level is a second account of the
- same fact.
-
-## Consequences
-
-**An author's whole contract is one line, once.** Write the migration in the module's code, name the
-entrypoint that runs it, and every later version rolls out as: build, prepare, run — with nothing
-per-version to remember and nothing about the machine to restate. That is the property this exists for.
-
-**Three hand-written steps in the catalogue collapse**, and the control plane's own migrate step stops
-repeating its server's environment and mounts.
-
-**The catalogue's self-preparation becomes the exception to remove.** One shape, and the mesh's own
-control plane is not an exception either.
-
-**A module scaled across machines with one shared state is not expressible**, and this record does not
-make it so. The mesh gives each consumer its own state; a deliberately shared one is a different
-provision model, and the place the "once, mesh-wide" question would genuinely return. Named here so it is
-a decision when it happens rather than a surprise.
-
-**There is still no readiness-gated step.** Only an action carries `verify`; nothing declares that a
-service answers, so preparation that must happen *after* something is serving — seeding through its own
-API — remains unexpressible.
-
-**Genesis keeps its own action.** At birth there is no control plane to derive anything, which is what
-[ADR 0067](0067-genesis-is-a-pivot.md) says about that moment.
-
-## How this is checked
-
-- **The composition carries the preparation, in the module's own context.** A test on a node's composed
- declaration: a version declaring a preparation entrypoint is preceded by it, and what it is given
- equals what the module's own code is given — asserted equal rather than written twice, which is the
- drift the superseded shape invited.
-- **A preparation that fails stops the version.** The host does not go past a step that did not complete,
- and the rollout stops at the first machine that did not take a version. Both are existing behaviours
- with existing tests; the test for preparation asserts the two together — the machine does not run it,
- and the machines after it are left alone.
-- **Once per version per state.** A test that a second convergence of the same version prepares nothing,
- and that a new version prepares again.
-- **The mesh's own control plane declares one.** The case that failed on 2026-09-28 is the case the tests
- cover, rather than a case a comment says is covered.
-
-## References
-
-- [ADR 0133](0133-a-module-owns-its-migrations-and-the-mesh-owns-when-they-run.md) — what this supersedes, and why
-- [ADR 0052](0052-a-step-that-runs-once-before-a-container.md) — the host-side step that implements it for an image artifact
-- [ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md), [issue 022](../04-ISSUES/022-one-credential-per-node-per-provision-not-per-module/00-report.md) — a consumer is a module on a machine, which is what makes the scope derivable
-- [ADR 0018](0018-a-picture-is-read-from-what-runs.md) — a digest is the record that something happened
-- [ADR 0005](0005-the-node-host.md) — the control plane decides and never touches a machine
-- [ADR 0134](0134-the-mesh-says-what-it-applied.md) — what makes a failed preparation visible
-- [issue 133](../04-ISSUES/133-the-control-planes-schema-is-migrated-at-birth-and-never-again/00-report.md) — the failure that produced both records
diff --git a/02-DECISIONS/0136-a-step-gates-its-module-not-the-machine.md b/02-DECISIONS/0136-a-step-gates-its-module-not-the-machine.md
deleted file mode 100644
index b0e73eb0..00000000
--- a/02-DECISIONS/0136-a-step-gates-its-module-not-the-machine.md
+++ /dev/null
@@ -1,106 +0,0 @@
----
-topic: what runs on it
-status: accepted
-date: 2026-09-28
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0052-a-step-that-runs-once-before-a-container.md
----
-
-# 136. A step gates its module, not the machine
-
-## Context
-
-[ADR 0052](0052-a-step-that-runs-once-before-a-container.md) made a run-once container a step the host
-runs to completion, and gave it the same reach a failed action has: it stops everything the declaration
-places after it. When the only steps on the mesh were a broker's seed and a forge's admin account, that
-reach was invisible — the thing after the step was the container the step existed for, in the same
-module.
-
-[ADR 0135](0135-a-module-version-prepares-its-state-before-it-runs.md) made a step something the mesh
-derives for **any** module that prepares its state, and that turns the reach into a fault. A module
-whose database is briefly unreachable now stops every module declared after it on that machine, for as
-long as it is unreachable.
-
-**The host already rejected this for every other shape, and says why in its own loop.** From
-[issue 011](../04-ISSUES/011-one-broken-module-blocks-every-other/00-report.md):
-
-> It used to stop at the first one, and that made one broken resource hold the whole machine hostage: a
-> module declaring a package that does not exist meant every module ordered after it was never applied,
-> for ever, and the mesh reported "failed" without saying that the rest had not been tried. A machine
-> with one bad module and nine good ones ran none of the nine.
-
-Everything is attempted and every failure reported — except an action and a run-once step, kept as the
-deliberate exceptions. So the mesh has two rules about the same question and the wider one is now
-reachable by any module that declares a schema.
-
-**And it deadlocks a case the catalogue already named.** The catalogue migrates its own schema when it
-starts rather than in a step, and says why in its code: *a schema step that had to reach the provider
-over the overlay would block the very apply that brings the overlay up*. With a machine-wide gate that
-is exactly right — the step fails, the apply stops, the overlay module after it is never applied, and
-the next reconcile is blocked the same way. The module that most obviously wants a step could not have
-one.
-
-## Decision
-
-**A step gates its own module.** A run-once container that does not complete stops the rest of *that
-module's* resources and nothing else. Every other module on the machine is attempted, as every other
-shape already is.
-
-**An action still gates the machine.** Genesis is a row of actions, each making the next possible, and
-they belong to no module — there is nothing narrower for their reach to be.
-
-**What was not attempted is reported, not inferred from silence.** A skipped resource appears in the
-machine's account of the apply as skipped, with the reason, because "not attempted" and "nothing to do"
-are different answers and only one of them is somebody's to fix.
-
-**A module is the part of a resource's identity before the first dot**, which is how the mesh composes
-them. What the mesh declares in its own right — a guard, an opening, the adoption's own resources —
-belongs to no module, and its gate is therefore the machine's.
-
-## Options considered
-
-1. **Leave the reach as it is.** Rejected: it reintroduces, through a mechanism now derived for every
- module, exactly the fault issue 011 removed. A mesh where one module's unreachable database stops a
- machine converging is worse than one where that module alone is behind.
-2. **Make preparation not a gate at all** — run it and carry on. Rejected: then a version serves against
- a state nobody shaped, which is the whole of what ADR 0135 exists to prevent.
-3. **Order every module's step before everything else on the machine**, so a gate stops nothing that
- matters. Rejected: it inverts the order a module needs — its files and directories are declared before
- its step because the step reads them — and it would still stop later modules.
-4. **Let a module declare how far its step reaches.** Rejected: the answer is the same for every module,
- and a field would let one be wrong about it.
-
-## Consequences
-
-**The catalogue can move to a step.** The reason it migrates at start — that a step blocks the apply
-that would make its provider reachable — stops being true: the step fails, that module waits, the
-overlay comes up, and the next reconcile prepares it. One shape for the whole mesh, which is what
-ADR 0135 asked for and could not have had.
-
-**A module can sit behind while the machine is otherwise current.** That is the honest state and it is
-what the report now says. It also means a preparation that never succeeds is a module that never
-upgrades, quietly, until somebody reads the report — which is an argument for
-[ADR 0134](0134-the-mesh-says-what-it-applied.md) rather than against this.
-
-**A module's resources must be ordered within the module for the gate to mean anything.** They already
-are: the mesh composes a module's resources in the order its manifest declares them, and its own
-workload comes after the files it reads.
-
-## How this is checked
-
-- **A failed step stops its module and nothing else.** A test with two modules: the one whose step
- failed does not start its workload, the other starts, and the error still says the failure gated
- something. It fails against the previous behaviour, which is how it was written.
-- **An action still stops the machine.** The existing test for a failed action is unchanged, and a step
- with no module in its identity — which is what genesis carries — takes the same path.
-- **The report names what was skipped.** Asserted in the same test, because a gate nobody can see is
- indistinguishable from a module that had nothing to do.
-
-## References
-
-- [ADR 0052](0052-a-step-that-runs-once-before-a-container.md) — the step this narrows
-- [ADR 0135](0135-a-module-version-prepares-its-state-before-it-runs.md) — what made the reach reachable
-- [issue 011](../04-ISSUES/011-one-broken-module-blocks-every-other/00-report.md) — the same fault, removed once already
-- [ADR 0134](0134-the-mesh-says-what-it-applied.md) — how a module left behind becomes visible
-- mesh-host `internal/apply` — the loop whose own comment argued this case for every other shape
diff --git a/02-DECISIONS/0137-a-machine-says-which-networks-it-routes.md b/02-DECISIONS/0137-a-machine-says-which-networks-it-routes.md
deleted file mode 100644
index 7500266b..00000000
--- a/02-DECISIONS/0137-a-machine-says-which-networks-it-routes.md
+++ /dev/null
@@ -1,117 +0,0 @@
----
-topic: what runs on it
-status: superseded
-date: 2026-09-28
-deciders: jochen
-extends: 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md
-reconstructed: false
-superseded-by: 02-DECISIONS/0140-the-filter-constrains-what-arrives-from-outside.md
----
-
-# 137. A machine says which networks it routes
-
-## Context
-
-The filter the mesh derives denies forwarding by default, because without a forward chain it says
-nothing about a container's published port
-([ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md), [issue 047](../04-ISSUES/047-the-firewall-does-not-cover-published-container-ports/00-report.md)).
-To keep a machine's own containers working it then allows two ranges: the container runtime's
-default bridge pool, and the pool its compose files are given. Those two are named in the
-controller's code, with a comment saying what the gap is:
-
-> A machine whose runtime is configured with something else needs this to say so — which is a thing
-> the mesh cannot derive and a reason this list is named here rather than computed.
-
-**There was no way to say so.** The list was a constant. A machine whose guests live anywhere else
-was filtered by a rule that looked deliberate and was a guess.
-
-**Measured, on the day a workstation was converged.** Flipping it cut egress for five of its
-container networks at once, and for every network its test beds create — the beds allocate a fresh
-range per run, from a pool neither default covers. Nothing reported a fault. The containers could
-not reach anything, the machine went on reporting that it had applied what it was told, and the
-converge preview had said nothing about it either, because the preview lists what *listens* and
-routing is not a listener.
-
-**And two questions, not one.** A guest also asks its host for an address and for names. Both arrive
-at the input chain, where nothing declared them, so denying by default left the guests of a routed
-network with no address and no resolution — which is not a closed port but a network that does not
-function, asked for by this machine's own guest.
-
-**Why the machine cannot simply be read.** A test bed creates its bridge while it runs, between one
-declaration and the next, so a filter derived from what the machine last reported would be correct
-only for the networks that already existed when it was composed. A declared range covers the ones
-that do not exist yet.
-
-## Decision
-
-**A machine says which networks it routes for what it hosts, and the filter forwards them.** A
-node-level fact, beside the node's public domain
-([ADR 0066](0066-public-routing-is-name-agnostic.md)) and for the same reason: the
-machine routes them, and the module that loads the filter holds a seat and may be replaced.
-
-**Added to the runtime's defaults, never replacing them.** A machine that names one range has not
-stopped hosting whatever was already on the runtime's own pools, and replacing would trade one
-silent breakage for another.
-
-**Their guests keep address and name service.** For a network that was named, the input chain admits
-that network's own DHCP and DNS, and nothing else: everything else a guest might want from its host
-is a port somebody declares, like every other port on this machine.
-
-**Said in CIDR form and checked when it is said.** An entry that does not parse is a line nftables
-refuses, and a refused ruleset is a machine filtering nothing while its unit reports a fault — so
-the refusal happens where a person can read it, not on the machine.
-
-**A machine that says nothing is filtered exactly as before.** Every machine already converged is
-untouched by this.
-
-## Options considered
-
-1. **Leave it constant and edit the code per installation.** Rejected: the value is a property of
- one machine, the code is the whole mesh's, and the two ranges as they stand describe a machine
- whose runtime was left at its defaults. It is also how this got here.
-2. **Derive it from what the machine reports.** Rejected as insufficient, not as wrong: it cannot
- cover a network created between two declarations, which is precisely the case that was broken. It
- would also make the filter follow whatever appeared on the machine, which is a firewall that
- widens itself.
-3. **A per-node setting on the module that loads the filter.** Rejected: the machine routes the
- networks. The filter module holds a node-scoped seat and is meant to be replaceable, and a
- replacement must not lose the machine's own truth.
-4. **Replace the defaults with what is said.** Rejected: see the decision. The first machine to name
- its bed range would lose its containers.
-5. **Admit all input from a routed network, not only address and name service.** Rejected: that is
- every port on the machine open to anything it hosts, which is the derivation abandoned.
-
-## Consequences
-
-**The converge preview says what a machine routes**, including when it routes nothing but the
-defaults, with the command that changes it. The preview's own sentence about traffic it cannot
-preview stays, because a tunnel and the found firewall's NAT are still not previewable.
-
-**A machine whose guests are already broken by an earlier flip is fixed by saying its networks and
-pushing**, with no flip to undo.
-
-**The list is one more thing that can be wrong and stale.** A range removed from the machine and
-left here keeps forwarding for a network that no longer exists, which admits nothing, because there
-is no guest on it to admit. That is the safe direction of being out of date.
-
-## How this is checked
-
-- **What a machine says it routes is forwarded, and its guests keep address and name service.** A
- test renders a ruleset for a machine that names one range and asserts both chains, per chain body
- so a line in the wrong chain cannot pass it. It fails against the previous behaviour, which is how
- it was written.
-- **The runtime's own defaults survive naming a range.** Asserted in the same test.
-- **A machine that names nothing renders byte-identically to one that names nil**, so every machine
- already behind this filter is untouched.
-- **Each family is matched in its own syntax.** A test with one v4 and one v6 network asserts
- `ip saddr` and `ip6 saddr`, because one set holding both is a syntax error and a ruleset that does
- not load is a machine filtering nothing.
-- **An entry that is not a network is refused where it is said**, by the parse in the setter.
-
-## References
-
-- [ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md) — the derived filter this completes
-- [ADR 0066](0066-public-routing-is-name-agnostic.md) — the precedent for a node-level fact
-- [issue 047](../04-ISSUES/047-the-firewall-does-not-cover-published-container-ports/00-report.md) — why there is a forward chain at all
-- [issue 137](../04-ISSUES/137-converging-a-machine-cut-off-its-own-guests/00-report.md) — the measurement that produced this
-- mesh-controller `internal/catalogue/filtering.go` — the constant whose own comment named this gap
diff --git a/02-DECISIONS/0138-an-assignment-binds-an-endpoint-and-says-how-far-it-reaches.md b/02-DECISIONS/0138-an-assignment-binds-an-endpoint-and-says-how-far-it-reaches.md
deleted file mode 100644
index 4996cc6f..00000000
--- a/02-DECISIONS/0138-an-assignment-binds-an-endpoint-and-says-how-far-it-reaches.md
+++ /dev/null
@@ -1,207 +0,0 @@
----
-topic: what runs on it
-status: accepted
-date: 2026-09-28
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0045-a-machine-firewall-is-the-sum-of-what-it-listens-on.md
-supersedes-in-part:
- - 02-DECISIONS/0045-a-machine-firewall-is-the-sum-of-what-it-listens-on.md
----
-
-# 138. An assignment binds an endpoint and says how far it reaches
-
-## Context
-
-[ADR 0045](0045-a-machine-firewall-is-the-sum-of-what-it-listens-on.md) settled that a machine's
-packet filter is derived from what its modules declare they listen on, and that the `from` of a
-listen "is the whole of public-versus-internal". That was true of the packet filter, and it turned
-out to be true of nothing else.
-
-Reachability is now settled three times, in three places, by three mechanisms that cannot disagree
-out loud ([issue 140](../04-ISSUES/140-an-endpoints-reach-is-not-declared/00-report.md)):
-
-- **The filter** reads a listen's source, and a per-node setting may override it. That setting has
- exactly one caller in the control plane — the function that builds the node's rules.
-- **The names** come from a route contribution, which names a label and a port and says nothing
- about reach. The reverse proxy composes a **public** name and an **internal** name for every route
- it is given, because it can.
-- **The certificate authority** follows from which names exist. Measured on the control-node: an
- identity provider carries a public certificate valid 90 days and an internal one valid 24 hours and
- renewed daily. No assignment asked for either.
-
-So *this endpoint must not be public* cannot be written. It is therefore enforced by nothing, while a
-public certificate for that very name is obtained automatically — the fault
-[how-we-build.md](../00-META/how-we-build.md) names, an unenforced rule being indistinguishable from
-a wrong one, with the additional cost that the wrong thing is done eagerly.
-
-And a port that is not routed cannot be spoken about at all beyond the filter. The forge serves git
-over ssh; that endpoint has no name, no certificate and no way to be called public except a key only
-the filter reads.
-
-**Two per-node settings already exist and are half of this.** One gives a module's declared port a
-machine port. One overrides a declared port's source. They key on port numbers, so nothing ties a
-port to the route that serves it: a route contribution names a port too, and the two are equal only
-by coincidence.
-
-**Where this belongs is already decided.** [ADR 0046](0046-a-module-configuration-is-its-assignments-not-its-manifest.md)
-says a module's configuration is its assignments. Whether the forge answers git-over-ssh from the
-public internet is a fact about one installation and one machine, not a property of the software —
-and [ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md) already refuses an
-installation's decisions in a definition.
-
-## Considered Options
-
-1. **Leave reach in the manifest, as `from` today.** Rejected: it is an installation's decision
- written into the definition, and it cannot differ between two machines running the same module —
- which is exactly the case the forge presents.
-2. **Extend the existing source override to the names and the certificate, without naming
- endpoints.** Rejected: it keys on a port number. A module's route contribution names a port as
- well, and nothing says the two are the same thing, so one statement cannot be made to reach all
- three mechanisms. Naming the endpoint is what makes that possible.
-3. **Derive reach from whether the node has a public domain recorded.** Rejected: that is a property
- of the machine, and two endpoints on one machine differ — a database and a web front end on the
- same host.
-4. **A fourth reach for "public name, internal authority"** — a name that resolves publicly and must
- not appear in a public issuance log, obtained by DNS-01. Deferred, not rejected: it is a real case
- and it is a question about which challenge an authority uses, not about how far an endpoint
- reaches. Left to the certificate work as an open question.
-5. **Make the manifest silent on reach and require every assignment to state it.** Rejected for the
- transition: every endpoint reachable today would close until an assignment named it, which is a
- flag day across the whole catalogue.
-
-## Decision
-
-**A module declares named endpoints.** An endpoint is one port the module serves, with a name the
-module chooses, its protocol, and what it is for. A route contribution **names the endpoint it
-routes** rather than repeating a port number. The manifest says what the module serves and what it
-would serve it to by default; it does not say what this installation does with it.
-
-**An assignment binds each endpoint and says how far it reaches.** Per node: the machine port the
-endpoint is published on, and its **reach** — one of `internal`, `public` or `both`. An assignment
-that states nothing keeps the manifest's default, so no machine changes until an assignment says so.
-
-**Reach means all three mechanisms at once, and is the only thing that decides them.**
-
-- `internal` — the filter opens the machine port to the private network; the proxy serves the
- internal name and not the public one; the certificate comes from the mesh's own authority.
-- `public` — the filter opens it to anywhere; the proxy serves the public name; the certificate
- comes from the public authority.
-- `both` — both names, each from its own authority, and the filter opens to anywhere.
-
-**An endpoint that is not routed is reached but never named.** An endpoint with no route contribution
-yields filter rules and nothing else: no name is composed and no certificate is requested. Git over
-ssh is that case, and it is the case the model could not express.
-
-**The authority stops being chosen by which names happen to exist.** The proxy composes the names the
-assignments asked for, and asks each name's own authority for it. A name nobody asked for is not
-composed, so it is not certified.
-
-**The two existing settings are this, completed.** The per-node port mapping becomes the endpoint's
-binding. The per-node source override becomes its reach, widened from the filter alone to the names
-and the certificate as well.
-
-## Progressive insight — 2026-09-29, from building it
-
-**Reach does not mean the same thing to the filter for an endpoint the proxy serves.** The decision
-above says `internal` means "the filter opens the machine port to the private network" and `public`
-means "the filter opens it to anywhere". For a routed endpoint the second half is wrong, and
-[ADR 0045](0045-a-machine-firewall-is-the-sum-of-what-it-listens-on.md) already said so before this
-record was written: *a public service is exposed through the proxy, not by opening its own port* — it
-listens `from: mesh`, only the proxy reaches it, and it is exposed by name.
-
-Found by trying to express one real module, not by review. Its routed name must be public, because
-browsers post to it; its machine-side port must not be, because that port serves the dashboard in
-cleartext. Under one value driving both, saying "public" would have reopened a port an operator had
-just closed. Measured the same evening: that module's routed name answered from the internet over TLS
-while its machine-side port was refused from the same place. The port is not the path.
-
-So the reach of a **routed** endpoint asks for names, and its port keeps what the manifest said. The
-reach of an **unrouted** endpoint — git over ssh, a mail port, the bus — governs the port, because
-there is no name and the port is the only way in. That is the same split this record already draws in
-*an endpoint that is not routed is reached but never named*; what it got wrong was carrying the filter
-across it.
-
-This corrects a fact, not the decision: one statement per endpoint, three things derived from it and
-none of them deciding on its own, all stand. The table in the decision should be read with the filter
-column applying to an unrouted endpoint.
-
-## Progressive insight — 2026-10-02, from issue 191
-
-**For a routed endpoint, "the proxy serves the internal name" has to mean "serves it to the private
-network", and only the proxy can make it mean that.** The decision says `internal` means the proxy
-serves the internal name and not the public one. It does not say to whom, and the proxy answered
-every name it routes to any request that carried it, on the same listeners as its public names. A
-name being internal kept nobody out: a request from the internet only had to send it. While every
-routed endpoint also had a public name, nothing showed it. Once an endpoint could be internal alone
-([issue 191](../04-ISSUES/191-a-route-with-only-an-internal-name-is-dropped/00-report.md)), serving
-its name to everyone would have published exactly what `internal` was chosen to keep private.
-
-The earlier insight above says the port is not the path for a routed endpoint. This is its other
-half: the proxy is the path, so the proxy is where `internal` is enforced. It serves an internal name
-only to the machines of the mesh and to the machine itself
-([ADR 0144](0144-anything-on-a-machine-may-call-anything-on-it.md)). Who the mesh is, it is told,
-not left to work out: its membership carries the same list of machine addresses the filter's "from the
-mesh" is rendered from ([ADR 0167](0167-a-membership-carries-what-its-module-receives-and-who-the-mesh-is.md)).
-To anyone else, the name is answered as one never routed, in the handshake and in the request, and not
-listed among the names it serves. This holds for the internal name of a `both` endpoint too, whose
-outsiders have its public name.
-
-The decision, the options and the consequences stand: one statement per endpoint, three things
-derived from it. Checked in the proxy's own tests: an internal-only name is served to a machine the
-membership names and to loopback, and refused, unlisted and uncertified for any other request; until
-the mesh is issued, it is served to the machine alone.
-
-## Consequences
-
-- **A manifest gains endpoint names, and a route contribution names an endpoint instead of a port.**
- Every routed module's manifest changes. The word ships one release before any manifest uses it, and
- reaches the build machine and the control plane first.
-- **One derived value is read by three things** — the filter's rules, the proxy's contributions, the
- certificate request — so they can no longer disagree, and a disagreement becomes a refusal at the
- assignment rather than a surprise on a machine.
-- **A name that must not be public becomes writable, and therefore checkable.** It also gives
- [issue 129](../04-ISSUES/129-nothing-makes-a-machine-trust-the-meshs-authority/00-report.md) a
- declared answer to read: which endpoints are internal is what says whose root must be installed
- where.
-- **[Issue 139](../04-ISSUES/139-an-internal-route-name-resolves-to-the-consumers-node/00-report.md)
- becomes answerable**: the endpoint's assignment names the machine that serves it, which is the fact
- the internal name should be composed from.
-- **Reach becomes reportable.** The mesh can say, per endpoint, where it is reachable from and which
- authority holds its certificate — neither of which `status` can say today.
-- **This narrows [ADR 0045](0045-a-machine-firewall-is-the-sum-of-what-it-listens-on.md).** Its
- decision stands: the firewall is derived and host-applied, not a provider. What no longer holds is
- that a listen's `from` is the whole of public-versus-internal; it is the filter's share of a
- statement that also governs names and certificates.
-- **What got harder:** every endpoint needs a name, including a module that serves exactly one port
- and had no reason to name it. And an installation that wants a module public must now say so on the
- assignment rather than inheriting it from the definition, which is more to say and the reason it is
- right.
-
-## How it is checked
-
-- **One module, two endpoints, different reach.** A module declaring an internal endpoint and a
- public one renders a filter opening one to the private network and one to anywhere, asserted per
- chain body so a rule in the wrong chain cannot pass.
-- **The names follow the reach.** The same module's routed endpoint composes the internal name only
- when internal, the public name only when public, and both when both — and a certificate is
- requested from the matching authority for each name composed and for no other. This fails against
- the previous behaviour, where both names and both certificates are always composed, which is how
- it is written.
-- **An unrouted endpoint is filtered and never named.** Asserted for an endpoint with reach and no
- route contribution: rules rendered, no contribution, no certificate request.
-- **An assignment naming an endpoint the module does not declare is refused where it is said**, as is
- a reach that is not one of the three — before it reaches a machine, because a ruleset that does not
- load is a machine filtering nothing.
-- **An assignment that states nothing renders byte-identically to today**, so every machine already
- converged is untouched until its assignment says otherwise.
-
-## References
-
-- [ADR 0045](0045-a-machine-firewall-is-the-sum-of-what-it-listens-on.md) — narrowed here
-- [ADR 0046](0046-a-module-configuration-is-its-assignments-not-its-manifest.md) — where reach belongs
-- [ADR 0066](0066-public-routing-is-name-agnostic.md) — the public name this composes
-- [ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md) — why reach is not a definition's
-- [issue 140](../04-ISSUES/140-an-endpoints-reach-is-not-declared/00-report.md) — the measurement
-- [issue 139](../04-ISSUES/139-an-internal-route-name-resolves-to-the-consumers-node/00-report.md),
- [issue 129](../04-ISSUES/129-nothing-makes-a-machine-trust-the-meshs-authority/00-report.md)
diff --git a/02-DECISIONS/0139-a-network-is-forwarded-because-a-module-declared-it.md b/02-DECISIONS/0139-a-network-is-forwarded-because-a-module-declared-it.md
deleted file mode 100644
index 00a9e6cb..00000000
--- a/02-DECISIONS/0139-a-network-is-forwarded-because-a-module-declared-it.md
+++ /dev/null
@@ -1,137 +0,0 @@
----
-topic: what runs on it
-status: superseded
-date: 2026-09-28
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0137-a-machine-says-which-networks-it-routes.md
-superseded-by: 02-DECISIONS/0140-the-filter-constrains-what-arrives-from-outside.md
----
-
-# 139. A network is forwarded because a module declared it
-
-## Context
-
-[ADR 0137](0137-a-machine-says-which-networks-it-routes.md), decided the same week, gave a machine a
-way to say which networks it routes for its guests. It was written because the derived filter's
-forward chain allowed two ranges named as constants in the control plane's source — the container
-runtime's bridge pool, and part of the pool its compose files are given — with a comment admitting
-the gap: *a machine whose runtime is configured with something else needs this to say so, which is a
-thing the mesh cannot derive.*
-
-**It can be derived, and from the right place.** Measured on the last machine still to be converged
-([issue 141](../04-ISSUES/141-the-forward-chain-does-not-follow-the-modules/00-report.md)): twenty-one
-container networks, nine inside the runtime's bridge pool, twelve in the other private range, and six
-of those outside the constant's lower bound — so the flip would have cut their guests off exactly as
-it did on the workstation that produced 0137.
-
-Naming a range to cover the six is what 0137 provides for, and it is the wrong instrument. Of those
-six networks, **four are networks the mesh's own modules declare**, present as network resources in
-the node's plan and created by the host because a module asked for them. **Two are the predecessor's
-leftovers** — compose networks of services the mesh does not run. Any range wide enough to keep the
-four forwards the two as well: a firewall widened by hand to protect networks that should not exist.
-
-The mesh already knows which of the twenty-one are its own, because it made them.
-
-**And the node's configuration is meant to follow the modules assigned to it.** That is the mesh's
-founding shape — the machine runs modules, and its files, its filter and its accounts are composed
-from what runs there ([ADR 0005](0005-the-node-host.md),
-[ADR 0010](0010-delivery.md),
-[ADR 0045](0045-a-machine-firewall-is-the-sum-of-what-it-listens-on.md)). The forward chain is the
-one derived thing that consults a constant and a list a person types.
-
-## Considered Options
-
-1. **Keep 0137 as it stands** — two constants plus a named list. Rejected: the list is written in
- addresses, and addresses are what the runtime allocates, so the only entry safe enough to keep a
- machine working is wider than the truth. It cannot distinguish a network the mesh made from one
- left behind, which is the distinction that decides whether forwarding it is correct.
-2. **Derive it from what the machine reports.** Still rejected, on 0137's own grounds: a test bed
- creates its bridge between one declaration and the next, and a filter that follows whatever
- appeared on a machine is a firewall that widens itself. **This decision is not that** — see below.
-3. **Have the control plane allocate each module network's range from a pool it owns,** so it can
- render the address itself. Rejected: more machinery for no gain. The runtime already allocates and
- the host already knows, and taking allocation over means the mesh owning an address space it has no
- other reason to own.
-4. **Have each module declare its network's range.** Rejected by
- [ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md): a definition names no address,
- and the same definition runs on machines whose runtimes have allocated differently.
-
-## Decision
-
-**A network is forwarded because a module declared it.** Per node, the forward chain forwards the
-networks of the modules assigned there, and by default nothing else. A module unassigned stops being
-forwarded at the next reconcile.
-
-**The host resolves a declared network to its addresses.** A network resource carries a name; the
-runtime allocates the subnet when the network is created. So the control plane declares *forward the
-networks these modules asked for* and the host — which made them, and already resolves a container by
-its name — renders the addresses. [ADR 0005](0005-the-node-host.md) holds: the host applies, it does
-not decide.
-
-**Deriving from the declaration is not deriving from the machine.** Both of 0137's objections fall
-away. The set is known before the network exists, because a module declared it, so a network created
-between two declarations is already in the one that asked for it. And it cannot widen itself: a
-network nobody declared is never forwarded, however it appeared on the machine.
-
-**The runtime's own default bridge is forwarded, from what the runtime reports.** Containers that name
-no module network attach to it, and it belongs to the runtime rather than to any module — so the host
-renders it from what the runtime says, not from a range named in the control plane. The constants go.
-
-**What a machine says is for guests no module declares.** A test bed is not a module and its range is
-not a module's; that is the case 0137's mechanism is for, and it keeps it — added to the derived set,
-never replacing it, as 0137 decided. Narrowed to that, it is named for it.
-
-**Their guests keep address and name service**, per declared network, unchanged from
-[ADR 0137](0137-a-machine-says-which-networks-it-routes.md): the input chain admits that network's own
-DHCP and DNS and nothing else.
-
-## Consequences
-
-- **The two constants are removed**, and with them the class of fault that a machine's guests depend
- on a range that describes some other machine.
-- **This is a behaviour change, not a refactor.** On the machine measured, the derived set and the
- constant do not cover the same ground — that is the whole reason for the record. A machine whose
- module networks happen to fall inside the old ranges renders the same rules.
-- **A range that exists only to keep a leftover alive becomes visible as such**, because it will not
- be in the derived set and has to be said out loud to survive.
-- **`node networks` narrows** to guests no module declares, and the preview says which of a machine's
- networks are the mesh's and which are not, so the difference is readable before a flip rather than
- after.
-- **A module's declaration gains nothing.** It already declares its network; what changes is that the
- filter reads it.
-- **What got harder:** the host renders part of the forward chain from what it created, so the
- control plane no longer holds the whole rule set as text. The rule the mesh states is the set of
- networks; the addresses are the machine's.
-
-## How it is checked
-
-- **Only declared networks are forwarded.** A node with two modules that declare networks renders
- forward rules for exactly those two, and none for a third network present on the machine that no
- module declared. This fails against the previous behaviour, which forwards by range and cannot tell
- them apart, and that is how it is written.
-- **Unassigning a module removes its network's rule** at the next reconcile, asserted on the rendered
- chain rather than on the intent.
-- **Guests of a declared network keep address and name service**, asserted per chain body so a line in
- the wrong chain cannot pass — carried from 0137.
-- **The runtime's own default bridge comes from the runtime**, asserted by rendering for a runtime
- whose default bridge is somewhere other than the range the constant named.
-- **A machine that names a range for guests no module declares still gets it**, added to the derived
- set and not replacing it.
-- **Each family is matched in its own syntax**, carried from 0137: one set holding both is a syntax
- error, and a ruleset that does not load is a machine filtering nothing while its unit reports a
- fault.
-
-## References
-
-- [ADR 0137](0137-a-machine-says-which-networks-it-routes.md) — narrowed here; its mechanism keeps the
- case it is right for
-- [ADR 0005](0005-the-node-host.md) — the host applies; the addresses are the machine's
-- [ADR 0045](0045-a-machine-firewall-is-the-sum-of-what-it-listens-on.md) — the filter is derived from
- what runs there
-- [ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md) — why a module does not name its
- range
-- [issue 141](../04-ISSUES/141-the-forward-chain-does-not-follow-the-modules/00-report.md) — the
- measurement
-- [issue 137](../04-ISSUES/137-converging-a-machine-cut-off-its-own-guests/00-report.md) — the
- breakage that produced 0137
diff --git a/02-DECISIONS/0140-the-filter-constrains-what-arrives-from-outside.md b/02-DECISIONS/0140-the-filter-constrains-what-arrives-from-outside.md
deleted file mode 100644
index e2028f24..00000000
--- a/02-DECISIONS/0140-the-filter-constrains-what-arrives-from-outside.md
+++ /dev/null
@@ -1,146 +0,0 @@
----
-topic: what runs on it
-status: accepted
-date: 2026-09-28
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0045-a-machine-firewall-is-the-sum-of-what-it-listens-on.md
-supersedes:
- - 02-DECISIONS/0137-a-machine-says-which-networks-it-routes.md
- - 02-DECISIONS/0139-a-network-is-forwarded-because-a-module-declared-it.md
----
-
-# 140. The filter constrains what arrives from outside, and says nothing about a machine's own guests
-
-## Context
-
-The filter the mesh derives blocks traffic passing *through* a machine unless something allows it,
-because a container's published port is traffic passing through rather than traffic arriving at the
-machine itself ([ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md),
-[issue 047](../04-ISSUES/047-the-firewall-does-not-cover-published-container-ports/00-report.md)).
-Having blocked all of it, the filter then had to let the machine's own containers reach outward again.
-It does that by listing the address ranges those containers sit on.
-
-As rendered on a converged workstation today:
-
-```
-policy drop
-ct state established,related accept
-ip saddr 172.16.0.0/12 accept
-ip saddr 192.168.128.0/17 accept
-ip saddr 10.0.0.0/8 accept
-ip saddr 192.168.16.0/20 accept
-... four more
-```
-
-Two of those ranges were constants in the control plane's source. The rest were typed by the operator
-after [ADR 0137](0137-a-machine-says-which-networks-it-routes.md), which existed to make the typing
-possible, because converging that workstation had cut every one of its containers off from the
-internet and nothing reported a fault
-([issue 137](../04-ISSUES/137-converging-a-machine-cut-off-its-own-guests/00-report.md)).
-
-**The list is the mistake, not its contents.** Every attempt to make it correct fails the same way.
-A constant describes one machine. A typed range goes stale, and cannot tell a network the mesh made
-from one a predecessor left behind — measured on the control-node, where six such ranges fall outside
-the constants and two of the six belong to services the mesh does not run
-([issue 141](../04-ISSUES/141-the-forward-chain-does-not-follow-the-modules/00-report.md)).
-[ADR 0139](0139-a-network-is-forwarded-because-a-module-declared-it.md) tried to generate the same
-list from the modules and put half the rule set on the machine to do it. Three records, one list, and
-the list should not exist.
-
-**Because the mesh has no policy about a container reaching outward.** What the filter is for is
-stated in [ADR 0045](0045-a-machine-firewall-is-the-sum-of-what-it-listens-on.md): which port is open,
-and to whom. That is about what arrives. A container of this machine's own opening a connection to
-something else is not a port being opened to anybody, and enumerating the addresses it might do so
-from is bookkeeping about the machine's internal plumbing, which the mesh neither owns nor can know.
-
-**The system being replaced never had this fault, and its rule says why.** The chain still protecting
-the control-node applies only to traffic arriving on that machine's outward link, and leaves
-everything else alone. The mesh's filter dropped that distinction and replaced it with a list of
-addresses.
-
-## Considered Options
-
-1. **Keep the list and generate it better** — from the modules' declared networks, or from what the
- machine reports. Rejected: [ADR 0139](0139-a-network-is-forwarded-because-a-module-declared-it.md)
- is that, and it puts part of the rule set on the machine, which makes the rule set partly the
- machine's and the derivation advisory.
-2. **Name the guest links instead of their addresses, and allow only those.** Rejected as more than is
- needed: it fails in the safe direction, but it is still a list that has to keep up with the
- machine, and the thing it protects against — a container reaching outward — is not a thing the mesh
- has a position on.
-3. **Do not block traffic passing through at all.** Rejected: that is
- [issue 047](../04-ISSUES/047-the-firewall-does-not-cover-published-container-ports/00-report.md),
- where a published port was reachable from anywhere because no rule mentioned it.
-4. **Constrain what arrives from outside, and nothing else.** Adopted.
-
-## Decision
-
-**The filter constrains traffic arriving from outside the machine, and says nothing about traffic that
-did not.** Traffic passing through the machine is allowed unless it arrived on one of the machine's
-outward links, in which case it is allowed only where a declared endpoint's reach admits it
-([ADR 0138](0138-an-assignment-binds-an-endpoint-and-says-how-far-it-reaches.md)). A container of this
-machine's own reaching anywhere is not filtered, because the mesh has no position on it.
-
-**A machine says which of its links face outside.** One node-level fact, reported by the machine the
-way it already reports the kind of firewall it found and the tunnel it carries — not a setting, not a
-list of addresses, and not something anybody types. It does not change when a module is added or
-removed, which is what separates it from the list it replaces.
-
-**A machine that has reported no outward link is sent no filter.** Rendering a rule around a link
-whose name is not known produces a rule set that does not load, which is a machine filtering nothing
-while its unit reports success. The refusal happens in the control plane, where a person reads it, and
-the machine keeps the filter it already has.
-
-**No addresses of the machine's own networks appear in the filter.** The two constants are removed and
-`node networks` is removed with them, along with everything any machine was told to say through it.
-Ports continue to follow the modules exactly as before: a module assigned to a machine opens the port
-its assignment says it reaches on, and nothing about a network is said anywhere.
-
-## Consequences
-
-- **Three records collapse into one rule.** 0137 and 0139 are superseded. What 0137 was right about —
- that converging a machine had silently cut off its own containers, and that nothing previewed it — is
- answered by removing the cause rather than by giving the operator a way to compensate for it.
-- **Every machine already converged loses its declared ranges and keeps working**, because the traffic
- those ranges allowed is now allowed by not having arrived from outside. The workstation's five ranges
- and the laptop's one are deleted rather than migrated.
-- **A machine's test beds stop being a special case.** A bed's network is created while the machine
- runs and was the case no list could cover; it is now covered by not being mentioned.
-- **A new fact travels in the report**, and the control plane refuses to compose a filter without it,
- so the order of the roll-out matters: the machines report before the control plane depends on it.
-- **A machine with more than one outward link says so**, and a machine that acquires one while the mesh
- is not looking is treated as internal until its next report. That window is the cost of this shape;
- it is bounded by the report interval, and it exists on machines whose outward link changes, which
- are the machines with nothing published to the outside.
-- **What got harder:** nothing in the declaration, and one more thing a machine must be able to work
- out about itself. A machine that cannot say which link faces outside cannot be given a filter.
-
-## How it is checked
-
-- **A machine's own container reaches outward with no network named anywhere.** A bed converges a
- machine carrying containers on several networks, none of them mentioned in any setting, and each
- reaches out afterwards. This fails against the previous behaviour, where the same flip cut them off,
- and that is how it is written.
-- **A port declared reachable from outside is reachable; one that is not, is not.** Probed from off the
- machine's private network, for a published port and for an undeclared one, before and after the flip.
-- **A network created after the filter was composed needs no new filter.** A network is made on the
- machine after its last declaration and a container on it reaches out, with nothing re-sent.
-- **No address of a machine's own networks appears in a rendered filter**, asserted on the text so a
- range cannot creep back in.
-- **A machine that reports no outward link is sent no filter, and the refusal names it** — asserted in
- the control plane, and that the machine's existing filter is left alone.
-- **A machine reporting two outward links has both constrained**, asserted per chain body so a rule
- covering one and not the other cannot pass.
-
-## References
-
-- [ADR 0045](0045-a-machine-firewall-is-the-sum-of-what-it-listens-on.md) — what the filter is for
-- [ADR 0138](0138-an-assignment-binds-an-endpoint-and-says-how-far-it-reaches.md) — what admits traffic
- arriving from outside
-- [ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md) — why traffic passing through is
- filtered at all
-- [ADR 0137](0137-a-machine-says-which-networks-it-routes.md),
- [ADR 0139](0139-a-network-is-forwarded-because-a-module-declared-it.md) — superseded here
-- [issue 137](../04-ISSUES/137-converging-a-machine-cut-off-its-own-guests/00-report.md),
- [issue 141](../04-ISSUES/141-the-forward-chain-does-not-follow-the-modules/00-report.md)
diff --git a/02-DECISIONS/0141-the-host-delivers-its-own-successor.md b/02-DECISIONS/0141-the-host-delivers-its-own-successor.md
deleted file mode 100644
index 42891148..00000000
--- a/02-DECISIONS/0141-the-host-delivers-its-own-successor.md
+++ /dev/null
@@ -1,171 +0,0 @@
----
-topic: what runs on it
-status: accepted
-date: 2026-09-29
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0005-the-node-host.md
----
-
-# 141. The host delivers its own successor, and versions live side by side
-
-## Context
-
-[Issue 142](../04-ISSUES/142-the-host-is-the-one-thing-the-mesh-does-not-deliver/00-report.md). A
-merge builds every changed module and the control plane — which is itself a module — and the result
-reaches the machines running it with nobody asking. The host is the exception: it is not a build
-target, no declaration delivers it, and every machine in this mesh runs a byte-identical binary that
-somebody built on a workstation and copied out.
-
-The half that *recovers* from a bad host exists. `internal/upgrade` can tell that the executable this
-process started from was replaced on disk, and it records which version last completed a reconcile.
-The launcher counts consecutive failed starts, calls a rollback at the limit, and treats a clean exit
-as the host standing aside so that the next loop runs whatever is on disk now. That supervision is
-complete and correct.
-
-Two things make it dead code:
-
-- **`Replaced()` is called by nothing but its own tests.** Nothing tells the running host that a
- successor is waiting.
-- **The rollback resolves a version through the machine's package manager** — `pacman -U` from the
- package cache. No machine here has the host installed as a package, so the recovery cannot run on
- any of them; and being written in one package manager's terms, it cannot run on two of the three
- operating systems the host is built for — [ADR 0005](0005-the-node-host.md) builds one binary per
- operating system, pinned at link time.
-
-**The record already points at the answer.** What is kept is a *version*, not a path. Keeping a
-version is only useful to something that can choose between versions present on the machine, which is
-what the package manager was being asked to do. The versions can simply be on disk.
-
-## Considered Options
-
-1. **Deliver the host as a package, as the rollback assumes.** Rejected: it needs a package built and
- a repository trusted per operating system, three of each, and the existing `package` resource
- asserts presence and deliberately never a version — "version is the package manager's business and
- the mesh does not hold a second opinion about it" — so it cannot ask for a particular host anyway.
- Heaviest of the three and the only one that is different on every machine.
-2. **Write the new binary over the running one.** Rejected on a fact: a running executable cannot be
- truncated, and `archive` opens what it unpacks with `O_TRUNC`. It could be made to write and
- rename, which is better hygiene and worth doing for its own sake, but it buys nothing here that
- option 3 does not, and it leaves rollback with nowhere to go back to.
-3. **Versions side by side; the newest retires the old.** Adopted.
-
-## Decision
-
-**A host version is delivered as an archive into a directory named for it, and never over a running
-one.** The declaration names it like any other archive — fetched by digest, the digest checked before
-anything is unpacked. Nothing new travels, no new resource kind, and no change to how archives are
-applied, because the path being written is not the path being executed.
-
-**The launcher starts the most recently delivered version.** That is what "the newest" means: the
-version whose directory arrived last. It reads no pointer and follows no link — the mesh creates no
-links ([ADR 0012](0012-the-mesh-creates-no-symlinks.md)) — and the version is in the path, so nothing
-has to be told what is running.
-
-**The running host stands aside for a successor, and only between reconciles.** Finding a newer
-version delivered, it finishes the reconcile it is in and exits cleanly. The launcher already reads a
-clean exit as exactly this and starts what is on disk now. A host that stood aside mid-apply is the
-half-configured machine this project exists to prevent, so the check happens at the boundary and
-nowhere else.
-
-**A version that completes a reconcile records itself, and retires what came before it.** The
-known-good record is written as it is today. Then versions older than the one before the running one
-are removed: the running version and its predecessor are kept, which is exactly what a rollback
-needs, and nothing else accumulates.
-
-**Rollback starts the previous version instead of reinstalling a package.** At the failure limit the
-launcher pins the known-good version and starts that, once. The second failure is still a different
-diagnosis — the previously working version does not run either, so it is the machine and not the
-binary — and the halt is unchanged. No package manager, no package cache, and the same script on every
-operating system.
-
-**A machine says which host version it is running,** on the report it already sends, beside the other
-facts it states about itself. Without it nothing can say a machine is behind, so "every machine
-current with its source" cannot include the host.
-
-## Progressive insight — 2026-09-29, the same day
-
-**The delivery is not "nothing new", and this record said it was.** The decision above stands and is
-built: versions side by side, the newest runs, the running host stands aside between reconciles, a
-completed reconcile retires what is older than the predecessor, rollback picks a directory. What was
-wrong was a claim about how a version reaches a machine. The paragraph on delivery said the
-declaration "names it like any other archive… nothing new travels, no new resource kind"; the second
-half is true and the first is not, because two things the delivery needs do not exist:
-
-- **Nothing can compile it.** A `bundle` artifact is compiled by a closed list of toolchains —
- typescript and python — whose own comment says adding a language is a decision, because a language
- used by *modules* needs an SDK carrying the broker client, the event envelope and tool serving. The
- host uses none of that: it is what applies modules, not one of them. So the obligation that list
- warns about attaches to a module written in a language, not to the language being buildable, and
- the control plane — also written in Go — is built as an image from a Dockerfile rather than through
- a toolchain at all.
-- **A version cannot reach the path.** An `archive` resource names a fixed path in the manifest, and
- nothing interpolates the built version into it, so nothing can ask for
- `…/versions//`.
-
-Neither changes what was decided, which options were weighed, or any consequence: the shape is
-unaffected and the host half is merged and tested. What it changes is the cost, which this record
-understated as none. The remaining work is a way to build the host and a way to name a version in a
-path, and until both exist nothing delivers a version and every machine takes the fallback — which is
-what every machine does today.
-
-> **Progressive insight — 2026-09-30. Both of those exist now.** The paragraph above named two missing
-> things and they are built: a Go toolchain, based on a new `mesh-tools-go` module so the compiler is
-> named and not pinned, and `${version}` in any value of a resource that uses an archive or a bundle.
-> The mesh compiles its own host and publishes it to its own registry, measured — a statically linked
-> stripped binary, fetched back out and run. **The cost was larger again than this note said**: three
-> more things in the path assumed one language or one shape, and a fourth was in the base image.
-> The account is [issue 142](../04-ISSUES/142-the-host-is-the-one-thing-the-mesh-does-not-deliver/01-progress.md).
->
-> The version in a path is the artifact's **digest**, not the commit this note's own wording would
-> suggest. Two builds of one commit are meant to be the same bytes, so a content-addressed version
-> means an unchanged build keeps the path it had; a commit-named one would move for an identical binary
-> and recreate everything reading it.
->
-> **Still nothing delivers a version to a machine.** The host is a module and builds, and declares no
-> resources, so the bundle sits in the registry and no machine is asked to take it. That is the next
-> piece, and the decision above is unchanged by any of this.
-
-## Consequences
-
-- **The host becomes a build target and a module** — a module whose resource is the next host, applied
- by the host that is running. The bootstrap is not circular because the two are different versions in
- different directories.
-- **Rollback becomes usable on every machine**, having been usable on none. It also stops being
- written in one operating system's terms.
-- **One copy by hand remains, once.** The first host that understands versioned directories cannot be
- fetched by a host that does not. That copy is the last, and it is the honest cost of the change
- rather than a step in the design.
-- **Two versions occupy disk instead of one.** About nine megabytes. The predecessor is the price of a
- rollback that does not depend on a cache somebody else may clean.
-- **What got harder:** a host must now be able to find its own successor and to judge when it is safe
- to stand aside. Both are between reconciles, which is the only moment the host is not mid-change.
-- **A machine that is never told a newer version keeps running what it has**, indefinitely and
- visibly, because its report says which version that is.
-
-## How it is checked
-
-- **A delivered version is run, and the old one is not.** A bed delivers a second version to a machine
- running the first; the host exits between reconciles, the launcher starts the new one, and the
- machine reports the new version. This fails against the previous behaviour, where nothing notices a
- delivered version at all.
-- **It stands aside between reconciles and never inside one.** Asserted by delivering a version while
- an apply is in flight: the apply completes, and the exit follows it.
-- **A version that will not start is rolled back to its predecessor, once**, and the second failure
- halts with the machine named rather than the binary — asserted with no package manager involved.
-- **A completed reconcile retires what is older than the predecessor**, and never the predecessor
- itself, because that is what a rollback needs. Asserted on the directory afterwards.
-- **The report names the running version**, asserted end to end rather than on the function that reads
- it, since the point is that the control plane can tell a machine is behind.
-- **The launcher picks the newest delivered version** with no pointer file and no link, asserted by
- delivering two and checking which runs.
-
-## References
-
-- [ADR 0005](0005-the-node-host.md) — the host, and what its supervision is for
-- [ADR 0010](0010-delivery.md) — a declaration is owned resources; this adds no kind to it
-- [ADR 0012](0012-the-mesh-creates-no-symlinks.md) — why the version is in the path
-- [ADR 0005](0005-the-node-host.md), *it is built per operating system* — why a rollback written in
- one package manager's terms was wrong for two of three
-- [issue 142](../04-ISSUES/142-the-host-is-the-one-thing-the-mesh-does-not-deliver/00-report.md) — the
- measurement
diff --git a/02-DECISIONS/0142-the-mesh-delivers-its-own-components-as-binaries.md b/02-DECISIONS/0142-the-mesh-delivers-its-own-components-as-binaries.md
deleted file mode 100644
index fb01e3fc..00000000
--- a/02-DECISIONS/0142-the-mesh-delivers-its-own-components-as-binaries.md
+++ /dev/null
@@ -1,158 +0,0 @@
----
-topic: the mesh
-status: accepted
-date: 2026-09-29
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0141-the-host-delivers-its-own-successor.md
----
-
-# 142. The mesh delivers its own components as binaries, not as container images
-
-## Context
-
-Measured on the control-node, 2026-09-29:
-
-| what | how it runs | publishes |
-|---|---|---|
-| host | a binary on the machine | — |
-| controller, catalogue, builder, vault | containers | nothing |
-| store, registry, broker | containers | ports |
-
-**The mesh's own software is delivered two ways, and the difference is not a property of the
-software.** The host and the controller are both written in the same language, both the mesh's own,
-both doing the mesh's own work. One is an image fetched from a registry. The other is a file somebody
-copied to four machines, owned by no package, built by nothing
-([issue 142](../04-ISSUES/142-the-host-is-the-one-thing-the-mesh-does-not-deliver/00-report.md)).
-
-**The reason is not a judgement about either, it is that images are the only delivery that works.**
-There is no way to put a binary on a machine. The host is hand-copied because of that, and the
-controller is an image because of that. Neither was chosen on its merits.
-
-What it costs, all of it measured rather than argued:
-
-- **Genesis must raise a container runtime before the control plane can exist.** The bundle carries
- three images and one of them is the controller, *"in the bundle for the same reason they are: there
- is nothing to fetch it with yet"*
- ([design 07](../03-DESIGN/01-to-be/07-the-foundation.md)). So the hardest moment in the mesh's life
- has a prerequisite that the thing being started does not need.
-- **Updating the control plane depends on the control plane.** Its image is fetched from the registry,
- which is a container the controller manages.
-- **A change to the host cannot be rolled out at all.** Every machine here runs a byte-identical
- hand-copied binary. A change merged yesterday reached none of them.
-- **Compiling the language the mesh is written in is not a capability of the builder.** The bundle
- toolchains are typescript — real, with a registered base module — and python, which is named in the
- list and absent from the catalogue. The controller is built as an image from a Dockerfile, which is
- the per-repository incantation the bundle toolchain exists to abolish
- ([design 18](../03-DESIGN/01-to-be/18-building-a-module.md)).
-
-The half that *receives* a binary safely is already built and tested
-([ADR 0141](0141-the-host-delivers-its-own-successor.md)): versions side by side in directories named
-for them, the newest run, the running one standing aside between reconciles, retirement keeping the
-predecessor, and a rollback that chooses a directory. What is missing is everything that puts one
-there.
-
-## Considered Options
-
-1. **Leave it as it is.** Rejected: it is not a design, it is the reach of one mechanism. And it is
- what makes a host change undeliverable.
-2. **Containerise the host too**, so everything is delivered one way. Rejected: the host is what
- starts the container runtime and what applies containers. A host in a container is the bootstrap
- problem made total, and the machine would have no way back from a bad one.
-3. **Deliver the mesh's components as operating-system packages.** Rejected for the reason
- [ADR 0141](0141-the-host-delivers-its-own-successor.md) rejected it for the host: a package and a
- trusted repository per operating system, three of each, and the `package` resource asserts presence
- and deliberately never a version.
-4. **Binaries for the mesh's own components, containers for third-party software.** Adopted.
-
-## Decision
-
-**The mesh's own components are delivered as binaries on the machine.** The host, the controller, the
-catalogue, the builder, the vault — the software this project writes. They are delivered by the
-mechanism [ADR 0141](0141-the-host-delivers-its-own-successor.md) built: an archive, fetched by
-digest, unpacked into a directory named for its version, with the running one standing aside between
-reconciles and a rollback that chooses the predecessor.
-
-**Third-party software stays a container.** The store, the registry, the broker. They are somebody
-else's build, they are already adopted as modules
-([ADR 0078](0078-the-store-and-broker-are-modules.md)), and an image is the right way to carry
-somebody else's software. **The container runtime remains required** — modules use it — so this
-removes a dependency from the control plane, not from the machine.
-
-**The builder compiles the languages the mesh is written in.** A toolchain for Go, with a base module
-providing the compiler, exactly as typescript has. The obligation the toolchain list warns about — an
-SDK carrying the broker client, the envelope and tool serving — attaches to a *module* written in a
-language, not to the language being compilable. None of these components is a module in that sense;
-the host is what applies modules.
-
-**An artifact says what it targets.** A compiled binary is per operating system, pinned at link time
-([ADR 0005](0005-the-node-host.md)), and a toolchain deliberately takes nothing from the module,
-because anything a module could override there it would be writing a Dockerfile to override. So the
-target is a property of the artifact rather than of the recipe, and one artifact declared per target
-is one build each.
-
-**A component's version comes from where it sits, not from its linker.** It is unpacked into a
-directory named for its version, so it can read its own version from its path. The stamp goes, and
-with it the need for a build to know what it will be called.
-
-**Genesis carries a binary reference where it carried an image reference.** The principle does not
-change — the bundle names a thing by digest and the host fetches it, pinned because nothing can
-resolve a version when no mesh exists — and the container runtime stops being a prerequisite for the
-control plane. It stays a prerequisite for the store and the broker, which is where it belongs.
-
-**The order is staged, and each step stands alone.** Compiling Go; an artifact naming its target;
-delivering a binary; the host as the first component delivered; the controller, catalogue, builder and
-vault out of their containers; genesis last. Genesis is last for the reason it is always last: it
-matters for a machine nobody has yet, and every earlier step is provable on a mesh that exists.
-
-## Consequences
-
-- **One delivery for the mesh's own software**, so a change to the host ships the way a change to the
- controller does, and neither is copied by hand.
-- **The control plane stops depending on a container runtime and on its own registry.** Both remain on
- the machine for other reasons; neither gates the control plane's own life any more.
-- **`Replaced()`, the known-good record and the launcher's rollback stop being dead code.** They were
- written for this and have been called by nothing but their tests.
-- **Four more components gain a rollback they do not have.** Today a bad controller image is recovered
- by an operator; under this it is recovered the way a bad host is.
-- **Two versions of each component occupy disk.** Around nine megabytes each. The predecessor is what a
- rollback needs.
-- **Genesis gets smaller, not larger.** One fewer image to carry and one fewer runtime to raise before
- the control plane.
-- **This does not make the components smaller or simpler.** They are the same programs; what changes is
- how they arrive. A reader expecting the containers to have been hiding complexity will not find any.
-- **What got harder:** the builder gains a language, artifacts gain a target, and the mesh gains a
- second kind of thing it must deliver correctly — one where getting it wrong takes the control plane
- down rather than a module. That is why the host is first: it is the component whose recovery is
- already built and tested.
-
-## How it is checked
-
-- **A component is delivered and runs, with nothing copied by hand.** A bed builds the host from its
- repository, delivers it to a machine running an older one, and the machine reports the new version.
- This fails today at the first step, because nothing builds it.
-- **Each target is built once and only the matching one is delivered.** Asserted by declaring an
- artifact per operating system and checking that a machine is offered the one it can run — a host
- built for another is what ADR 0005's link-time pin exists to refuse.
-- **A component reads its version from its path**, asserted by unpacking the same bytes into two
- differently named directories and seeing each report its own.
-- **A bad component is rolled back without an operator**, for the host first: a version that will not
- start is replaced by its predecessor once, and the second failure halts naming the machine.
-- **The control plane comes up with no registry reachable**, which is the dependency this removes —
- asserted by raising it with the registry stopped.
-- **Genesis raises a control plane with no container runtime running**, and raises the store and the
- broker afterwards. Last, and on a machine with nothing on it.
-- **A published port count that does not change.** The mesh's own components publish nothing today, so
- moving them out of containers must not open anything — asserted on the machine's reachable set before
- and after, which the converge preview already reads.
-
-## References
-
-- [ADR 0141](0141-the-host-delivers-its-own-successor.md) — the receiving half, already built
-- [ADR 0005](0005-the-node-host.md) — the host, its supervision, and one binary per operating system
-- [ADR 0078](0078-the-store-and-broker-are-modules.md) — why third-party software stays a container
-- [ADR 0006](0006-the-substrate-and-the-control-plane.md) — what genesis must raise, and in what order
-- [issue 142](../04-ISSUES/142-the-host-is-the-one-thing-the-mesh-does-not-deliver/00-report.md) — the
- measurement that started this
-- [design 07](../03-DESIGN/01-to-be/07-the-foundation.md) — the bundle's three images, one of them the
- controller
diff --git a/02-DECISIONS/0143-a-consumer-verifies-the-grant-it-is-given.md b/02-DECISIONS/0143-a-consumer-verifies-the-grant-it-is-given.md
deleted file mode 100644
index 1e3a3f26..00000000
--- a/02-DECISIONS/0143-a-consumer-verifies-the-grant-it-is-given.md
+++ /dev/null
@@ -1,135 +0,0 @@
----
-topic: what runs on it
-status: superseded
-date: 2026-09-29
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0010-delivery.md
-superseded-by: 02-DECISIONS/0144-anything-on-a-machine-may-call-anything-on-it.md
----
-
-# 143. A consumer verifies the grant it is given
-
-## Context
-
-A **grant** is what the mesh writes on a consumer's machine so it can reach a provider. The real one
-the forge receives for its database, as it arrives:
-
-```
-provision postgres-database
-at
-port the machine port the provider is published on
-as the role the provider created for this consumer
-```
-
-with the credential sealed in a separate file. Four facts and a password, and they are the whole
-mechanism by which anything in the mesh reaches anything else.
-
-**The mesh asserts that claim and never finds out whether it is true.**
-[Issue 145](../04-ISSUES/145-a-machine-reads-healthy-while-its-modules-cannot-reach-each-other/00-report.md):
-converging a machine dropped the path from a container to a port on its own machine, and for eleven
-hours the mesh answered *all doing what they were told, all heard from, every module current with its
-source* while a web application logged, six thousand times:
-
-```
-connection to server at "" (10.10.0.1), port 6852 failed: timeout expired
-```
-
-Every check the mesh makes passed, because every check it makes is about the relationship between the
-mesh and a machine: the declaration was applied, the digest matched, every container named was running.
-None of them asks whether a consumer can reach what it requires — though the mesh composed the grant
-and therefore knows the consumer, the machine, the address, the port and the credential.
-
-**And where the check runs decides whether it catches anything.** The rule in force admitted the
-machines' own addresses on the private network. A dial from the *machine* to its own address carries
-exactly such a source address, so a check run by the host on its own behalf would have matched that rule
-and passed — while every container on the machine was refused. This is inference from the rule that was
-loaded, not a measurement: the fault was found and fixed before anyone thought to dial from the host.
-It is enough to decide the question, because a check whose position differs from the consumer's is
-testing something nobody asked about.
-
-## Considered Options
-
-1. **The control plane dials each provision.** Rejected, and it is the tempting one because the control
- plane holds every fact. It sits on the provider's machine for most provisions here and reaches the
- address by a path no consumer uses; in the measured outage it would have passed throughout.
-2. **The host dials on the consumer's behalf, from the machine.** Rejected for the reason above: the
- machine's network position is not the consumer's, and the one outage this exists to catch is exactly
- a difference between them.
-3. **Ask the module.** Rejected: a module is arbitrary software that the mesh does not write. Some could
- report on their provisions and most cannot, and a check that covers the modules that opted in tells
- nobody anything about the rest.
-4. **Read the module's logs.** Rejected: the failure was in a log the whole time, and reading a module's
- logs makes the mesh depend on the wording of software it does not control.
-5. **The consumer verifies it, from its own network position.** Adopted.
-
-## Decision
-
-**A consumer verifies each grant it is given, from its own network position.** After a reconcile has
-applied a grant, the machine opens a connection to the address and port that grant names, from inside
-the consumer's own network namespace — the same position the consumer's software dials from, which is
-the only position that answers the question the grant asks.
-
-**It is a connection, not a conversation.** Whether the port accepts a connection is what a grant
-claims; whether the credential is right, the role exists or the schema is current is the provider's to
-answer and the consumer's to discover. A check that spoke each provision's protocol would be a second
-implementation of every provision, and would fail for reasons that are not the mesh's.
-
-**One failure is not news.** A provider restarting is ordinary, and so is a consumer between containers.
-A grant is reported unreachable only after it has failed on **consecutive** reconciles, and the count is
-what the machine reports rather than the last attempt — so a reader can tell "it was briefly away" from
-"it has never worked".
-
-**A grant that cannot be checked is said to be unchecked, never assumed good.** A consumer that is not
-running has no network position to dial from; that is not a broken grant and must not read as one. It is
-also not a verified grant, and the two are different sentences.
-
-**What it costs to be wrong is the constraint on all of it.** A check that reports a working provision
-broken trains a reader to ignore the report, which is worse than having none — the fault this
-repository keeps finding, one level up. So the threshold is consecutive failures, the check is the
-cheapest thing that answers the question, and an unknown is reported as unknown.
-
-**The mesh says it where it says everything else.** A machine's report carries its unreachable grants,
-and `status` names them beside what is out of date — so "every module current with its source" stops
-being the whole of what the mesh will tell you about a machine whose modules cannot reach each other.
-
-## Consequences
-
-- **The mesh can be wrong out loud.** It has been able to assert a grant and not check it; now a grant
- that does not work is a thing the mesh says, and the eleven hours of issue 145 become minutes.
-- **The host gains the ability to act from a container's network position**, which it has not needed
- before. That is a real capability and the only one this needs.
-- **A machine reports something that is not about the declaration.** Everything it reports today is
- what it applied and what it holds; this is the first thing it says about whether what it applied
- works.
-- **A provision with no port is not checked**, because there is nothing to dial. Several are files and
- secrets, and saying "checked" about those would be the appearance of verification that this record
- exists to remove.
-- **What got harder:** a reconcile does more than apply. Every grant adds a connection attempt on a
- cadence, which is cheap individually and worth naming: a machine with many consumers dials once per
- grant per reconcile.
-
-## How it is checked
-
-- **The outage is caught.** A bed drops the path from a consumer's network position to a provider's
- port while leaving the machine's own path to it open — the exact shape of issue 145 — and the grant
- reads unreachable. This fails against the previous behaviour, where nothing reported anything, and
- against a check run from the machine, which passes while the consumer cannot reach it.
-- **A restarting provider is not an outage.** One failed reconcile reports nothing; the count rises and
- falls, and the grant reads reachable again without anybody acting.
-- **A consumer that is not running reads unchecked, not broken**, asserted separately from the
- unreachable case because they are different sentences.
-- **A provision with no port is not claimed to be checked.**
-- **The report carries the count, not the last attempt**, so "briefly away" and "never worked" are
- distinguishable by a reader who sees only the report.
-- **`status` names an unreachable grant**, asserted on the output, since a check nothing surfaces is
- the same as no check.
-
-## References
-
-- [ADR 0010](0010-delivery.md) — the declaration is owned resources; a grant is one of them
-- [ADR 0009](0009-modules-and-the-graph.md) — what a provision and a consumer are
-- [issue 145](../04-ISSUES/145-a-machine-reads-healthy-while-its-modules-cannot-reach-each-other/00-report.md)
- — the eleven hours
-- [issue 136](../04-ISSUES/136-a-module-may-name-a-program-the-machine-does-not-have/00-report.md) — the
- same distance between a declaration and a machine, one level down
diff --git a/02-DECISIONS/0144-anything-on-a-machine-may-call-anything-on-it.md b/02-DECISIONS/0144-anything-on-a-machine-may-call-anything-on-it.md
deleted file mode 100644
index f958e791..00000000
--- a/02-DECISIONS/0144-anything-on-a-machine-may-call-anything-on-it.md
+++ /dev/null
@@ -1,121 +0,0 @@
----
-topic: what runs on it
-status: accepted
-date: 2026-09-29
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0045-a-machine-firewall-is-the-sum-of-what-it-listens-on.md
-supersedes: 02-DECISIONS/0143-a-consumer-verifies-the-grant-it-is-given.md
----
-
-# 144. Anything on a machine may call anything on it, and that is the whole of "local"
-
-## Context
-
-Everything in the mesh should be able to call:
-
-- what runs on the same machine;
-- another machine's service over the private network, if that service is exposed there;
-- another machine's service over the public network, if it is exposed there.
-
-Three cases. The filter had two of them.
-
-**The first was broken and the break was invisible.** A service exposed to the private network rendered
-as the machines' own addresses on it. A caller on the machine carries such an address; a caller inside
-one of that machine's containers carries a bridge address and matched nothing. Measured:
-
-```
-the machine: local 10.10.0.1 dev lo src 10.10.0.1
-a container: 10.10.0.1 via 172.17.0.1 dev eth0 src 172.17.0.8
-```
-
-Same destination, same machine, two source addresses. The rule named the first and silently refused the
-second, so a module reaching its database on its own machine's name timed out for eleven hours
-([issue 145](../04-ISSUES/145-a-machine-reads-healthy-while-its-modules-cannot-reach-each-other/00-report.md)).
-
-**The second case works, and by accident.** A caller on another machine reaches the private network over
-the tunnel, and arrives carrying that machine's own address — so the rule matches. It would not have
-matched the caller's own address either; the tunnel rewrites it. That two of three cases worked is why
-this looked correct.
-
-**[ADR 0143](0143-a-consumer-verifies-the-grant-it-is-given.md) answered the wrong question.** Written
-hours earlier, it proposed that a consumer verify each grant it is given by opening a connection from
-its own network position — and it went to some length about *which* position, because whether a caller
-sat in a container changed the answer. That difference was the bug. A verification mechanism would have
-reported this outage sooner and would not have prevented it, and the machinery it needed existed only
-because the rule was wrong. The remedy for a configuration error is the correct configuration.
-
-**And a module is not a container.** A module is software that delivers one or more services, and it may
-do that as a container, an installed package with a unit, a binary, or files something else reads. Of 72
-modules in the catalogue, 61 happen to use a container and 11 do not — among them the resolver, the ssh
-daemon and the intrusion-prevention module. A rule that reasons about containers describes most of the
-mesh and not the mesh.
-
-## Considered Options
-
-1. **A line per service admitting the machine's own callers.** Rejected: it is what was written first,
- and it only ever covers the services somebody remembered to think about. It also states, service by
- service, a thing that is true of the machine.
-2. **Verify each grant from the consumer's position** ([ADR 0143](0143-a-consumer-verifies-the-grant-it-is-given.md)).
- Rejected as a remedy: it observes the fault rather than removing it, and the question it agonised over
- — which network position — exists only while the fault does.
-3. **Enumerate the addresses a machine's callers may have.** Rejected for the reason no address is named
- anywhere in this filter any more ([ADR 0140](0140-the-filter-constrains-what-arrives-from-outside.md)):
- a range describes one machine and goes stale in silence.
-4. **Local is not filtered, stated once.** Adopted.
-
-## Decision
-
-**Anything on a machine may call anything on that machine, and the filter says so once.** Not per
-service, not per port, and not by naming who the callers are: traffic that did not arrive from outside
-the machine and did not arrive over the private network is the machine's own, and is admitted. It is
-asked by the link the traffic arrived on, because that is a fact about the machine rather than a list
-that describes one.
-
-**Local is not a boundary this mesh draws.** Whether a caller is a container, a unit, or the operator's
-shell changes nothing, because the thing being decided is "is this the same machine" and the answer does
-not depend on the form the caller takes.
-
-**The other two cases are unchanged and are now legible beside it.** A service exposed to the private
-network admits the machines on it; a service exposed publicly admits anything. Three cases, three lines,
-and a reader can see all three at once.
-
-**[ADR 0143](0143-a-consumer-verifies-the-grant-it-is-given.md) is superseded and nothing replaces it.**
-Whether the mesh should check that a grant works is a real question — it reported this machine healthy
-for eleven hours — but it is a question about what the mesh can say, not about what it should do, and it
-must stand on its own rather than as the remedy for a rule that was wrong. It is not built.
-
-## Consequences
-
-- **The three things everything should be able to call are three lines**, and the first is one line
- rather than one per service, so a service added tomorrow is reachable locally without anybody
- remembering to say so.
-- **A form of module stops mattering to the filter.** The 11 modules that are not containers were never
- affected by this bug and were never the reason it was hard to see; they are the reason the rule should
- never have mentioned containers.
-- **The mesh still cannot say when a grant stops working.** That is the live gap, recorded in issue 145
- and no longer pretending to have an answer.
-- **What got harder:** nothing. This removes a line per service and replaces it with one.
-
-## How it is checked
-
-- **A caller on the machine reaches a service on it, in the input chain**, asserted on that chain's own
- body — because the forward chain carries the same line in the same words, and an assertion on the
- whole rendered file passed with the input chain's copy deleted. That is what
- [ADR 0137](0137-a-machine-says-which-networks-it-routes.md)'s tests already say to do.
-- **It is one rule, not one per service.** Asserted by rendering two services of different reach and
- refusing a per-port local line.
-- **The three reaches render as three lines**, asserted together, so the whole of what the filter says
- about who may call what is one test.
-- **The measured case:** from a container on the machine, a service exposed to the private network on
- that machine answers. This is the outage, and it fails against the rule this replaces.
-
-## References
-
-- [ADR 0045](0045-a-machine-firewall-is-the-sum-of-what-it-listens-on.md) — the filter is the sum
- of what its modules listen on
-- [ADR 0140](0140-the-filter-constrains-what-arrives-from-outside.md) — why no address is named
-- [ADR 0138](0138-an-assignment-binds-an-endpoint-and-says-how-far-it-reaches.md) — internal and public,
- the other two cases
-- [ADR 0143](0143-a-consumer-verifies-the-grant-it-is-given.md) — superseded here
-- [issue 145](../04-ISSUES/145-a-machine-reads-healthy-while-its-modules-cannot-reach-each-other/00-report.md)
diff --git a/02-DECISIONS/0145-a-module-checks-what-the-mesh-claims-is-reachable.md b/02-DECISIONS/0145-a-module-checks-what-the-mesh-claims-is-reachable.md
deleted file mode 100644
index 06b04fa5..00000000
--- a/02-DECISIONS/0145-a-module-checks-what-the-mesh-claims-is-reachable.md
+++ /dev/null
@@ -1,120 +0,0 @@
----
-topic: what runs on it
-status: superseded
-date: 2026-09-29
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0144-anything-on-a-machine-may-call-anything-on-it.md
-superseded-by: 02-DECISIONS/0146-connectivity-is-checked-by-name-per-hosting-form.md
----
-
-# 145. A module checks what the mesh claims is reachable, and it checks itself
-
-## Context
-
-The mesh asserts three things are callable ([ADR 0144](0144-anything-on-a-machine-may-call-anything-on-it.md)):
-what runs on the same machine, another machine's service exposed to the private network, and another
-machine's service exposed publicly. It has never checked any of them.
-
-[Issue 145](../04-ISSUES/145-a-machine-reads-healthy-while-its-modules-cannot-reach-each-other/00-report.md):
-the first of the three was broken for eleven hours and the mesh answered *all heard from, every module
-current with its source* throughout. Every check it makes is about the relationship between the mesh and
-a machine — applied, current, containers running — and none about whether anything can reach anything.
-
-**A first answer was drafted and withdrawn.** [ADR 0143](0143-a-consumer-verifies-the-grant-it-is-given.md)
-put the check inside the host, verifying each grant from the consumer's network position. It was
-superseded because the difference it worked so hard to reproduce — whether a caller sat in a container —
-was the bug itself. What survives from it is the part that was right: a check run from the wrong place
-proves nothing, and the mesh's own reports are not evidence about the network.
-
-**The mesh already has the shape for this and it is a module.** A module can declare a container that
-runs on a cadence ([ADR 0053](0053-a-step-that-runs-on-a-schedule.md), and three modules already use
-`*/5 * * * *`), can be given the mesh's roster as a rendered fact — every machine's name, address and
-this node's own identity, the same mechanism the resolver and the operator's ssh configuration use — and
-can emit what it found on the bus. Nothing new is needed to build this except the module.
-
-**What it must not check is the trap.** The obvious probe target is ssh: present on every machine, never
-closed by design. Dialling it would have passed throughout the outage, because ssh is admitted
-unconditionally and the thing that broke was a service exposed to the private network. A checker whose
-probe is unconditionally open measures the one path that cannot fail, which is the failure this whole
-sequence keeps producing — a check that reads as verification and verifies nothing.
-
-## Considered Options
-
-1. **The host verifies each grant** ([ADR 0143](0143-a-consumer-verifies-the-grant-it-is-given.md)).
- Superseded. It needed the host to act from another network position, which is machinery that exists
- only while local calls are filtered wrongly.
-2. **The control plane dials every node.** Rejected: it sits on one machine and reaches the others by a
- path no ordinary caller uses. It would have passed throughout the outage.
-3. **Probe an existing service.** Rejected for the target problem above: the services guaranteed on every
- machine are the ones that are never closed, so they cannot fail the way the mesh fails.
-4. **A module on every machine that serves its own probe and dials the others'.** Adopted.
-
-## Decision
-
-**A module runs on every machine, serves an endpoint of its own, and dials every other machine's.** The
-probe is the module's own endpoint, declared reachable over the private network — so the thing being
-dialled is admitted by exactly the rule that governs every other internally-exposed service, and fails
-when that rule is wrong. A second endpoint, declared public, does the same for the public path where a
-machine has one.
-
-**It checks the three cases the mesh claims, by name:**
-
-- its **own machine**, by dialling its own machine's address — the case that broke, and the only one that
- distinguishes a caller on the machine from a caller in one of its containers;
-- **each other machine over the private network**;
-- **each machine's public path**, where one is recorded.
-
-**It resolves before it dials, and says which failed.** A name that does not resolve and a port that does
-not answer are different faults with different owners, and a checker that reports one sentence for both
-sends a reader to the wrong place.
-
-**It runs where the callers run.** The module's own code in its own container, on the cadence the mesh
-already has, from the same position as every other module on that machine. It is not the host and not the
-control plane, and that is the whole point.
-
-**It says what it found and nothing else.** It emits results; it repairs nothing, opens nothing and holds
-no credential beyond its own. A checker that fixes things is a second control plane.
-
-**One failure is not a fault.** A machine rebooting is ordinary. A path is reported broken after it has
-failed on consecutive runs, and the count travels with the result so a reader can tell "briefly away"
-from "never worked" — the one thing [ADR 0143](0143-a-consumer-verifies-the-grant-it-is-given.md) got
-right and worth keeping.
-
-## Consequences
-
-- **The mesh gains the ability to be wrong out loud about the network.** Eleven hours becomes two runs.
-- **It is a module, so it is assigned, built, pushed and reported on like everything else** — no new host
- capability, no new vocabulary, nothing in the control plane that has to know about checking.
-- **Its own endpoint is the instrument.** That is what makes it able to fail; it also means the checker
- must be assigned to a machine before that machine can be checked, and a machine without it is
- unchecked rather than healthy.
-- **It cannot check what it cannot be told.** The roster gives it machines; it does not give it every
- module's endpoints, so this checks the paths the mesh claims and not every grant in the mesh. That is
- the honest scope of a first one, and the difference is worth saying rather than growing quietly.
-- **What got harder:** one more module on every machine, and a module whose whole purpose is to fail
- visibly when something else is wrong. Its own failures will be read as the mesh's, which is the cost of
- an instrument.
-
-## How it is checked
-
-- **It catches the measured outage.** A bed closes the path from a container to a service exposed to the
- private network on its own machine — issue 145's shape — and the checker reports its own machine
- unreachable while every other path still reads reachable. This fails against a probe on a port that is
- never closed, which is the wrong target this record exists to name.
-- **A machine rebooting is not a fault**: one failed run reports nothing, the count rises and falls.
-- **A name that does not resolve is reported as that**, not as a port that did not answer.
-- **It reports and does not act**: asserted by giving it a broken path and checking nothing on the machine
- changed.
-- **A machine without the module reads unchecked**, never healthy — asserted on what the mesh says about
- a machine it is not assigned to.
-
-## References
-
-- [ADR 0144](0144-anything-on-a-machine-may-call-anything-on-it.md) — the three things that must be callable
-- [ADR 0143](0143-a-consumer-verifies-the-grant-it-is-given.md) — superseded; what survives is that the
- position matters
-- [ADR 0053](0053-a-step-that-runs-on-a-schedule.md) — the cadence
-- [ADR 0138](0138-an-assignment-binds-an-endpoint-and-says-how-far-it-reaches.md) — internal and public,
- which the probe endpoints declare
-- [issue 145](../04-ISSUES/145-a-machine-reads-healthy-while-its-modules-cannot-reach-each-other/00-report.md)
diff --git a/02-DECISIONS/0146-connectivity-is-checked-by-name-per-hosting-form.md b/02-DECISIONS/0146-connectivity-is-checked-by-name-per-hosting-form.md
deleted file mode 100644
index a1f88b0a..00000000
--- a/02-DECISIONS/0146-connectivity-is-checked-by-name-per-hosting-form.md
+++ /dev/null
@@ -1,125 +0,0 @@
----
-topic: what runs on it
-status: accepted
-date: 2026-09-29
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0145-a-module-checks-what-the-mesh-claims-is-reachable.md
-supersedes: 02-DECISIONS/0145-a-module-checks-what-the-mesh-claims-is-reachable.md
----
-
-# 146. Connectivity is checked by name, per hosting form, with a valid certificate
-
-## Context
-
-[ADR 0145](0145-a-module-checks-what-the-mesh-claims-is-reachable.md) decided that a module checks what
-the mesh claims is reachable, from where the callers are, because the mesh reported four machines healthy
-for eleven hours while a module could not reach its database
-([issue 145](../04-ISSUES/145-a-machine-reads-healthy-while-its-modules-cannot-reach-each-other/00-report.md)).
-That decision stands. What it got wrong is everything about *what* is dialled.
-
-It dialled a raw port on each machine's address. Three things are wrong with that:
-
-- **A raw port is not how anything in this mesh is reached.** A real caller resolves a name, the proxy
- answers it, and the proxy reaches the service. A check that dials a port tests the last hop of a path
- with four hops in it, and the three it skips — resolution, the proxy, the certificate — are where most
- of the mesh's connectivity actually lives.
-- **It tested one hosting form.** A module is software that delivers services, and it may deliver them
- from a container, from a unit the mesh writes for its own code, or from a unit a package ships. Those
- are three different paths to the same machine, and the outage that produced this was two of them
- disagreeing. A probe served one way measures one way.
-- **It said nothing about certificates.** An internal name that resolves, routes and answers over TLS
- that nothing can verify is not a working path; it is a working path for whoever holds the proxy's
- trust and nobody else.
-
-## Decision
-
-**Each hosting form gets its own endpoint, its own route and therefore its own name.** On every machine:
-
-| name | what serves it |
-|---|---|
-| `connect-docker..internal` | a container |
-| `connect-process..internal` | the mesh's own code, in a unit the mesh writes |
-| `connect-unit..internal` | a unit a package ships |
-
-and the same set under each machine's public domain where it has one — `connect-docker.` and its
-siblings. The names are the instrument: a failure reads as *`connect-docker.g14.internal` did not answer*,
-which says which machine and which hosting form without anybody interpreting anything.
-
-**Every machine checks every machine, by name, over TLS, verifying the certificate.** Not a port, not an
-address: resolve the name, connect, complete the handshake, check the certificate against the authority
-that should have issued it — the mesh's own for an internal name, a public one for a public name. That is
-the whole path a real caller takes, and each step failing is reported as itself.
-
-**No name is written anywhere.** The machines come from the roster the mesh already renders as a fact, and
-the labels are the module's. A machine that joins appears in every other machine's roster on the next
-push, and they begin checking it without an edit.
-
-**And the module arrives on a machine because the machine exists, not because somebody assigned it.** A
-machine that joins and does not have it is worse than unchecked: every other machine is already dialling
-its names, so it reads as broken everywhere until someone notices. This is the part the mesh cannot
-currently express — see below — and it is the part that makes the rest safe.
-
-**What survives from 0145**, unchanged: it reports and repairs nothing; one failure is not a fault and a
-path is broken after consecutive runs with the count travelling with the result; findings are said on the
-bus, because a finding in a file on the machine is what this exists to end; and the bus is the one path
-that cannot report its own failure, so an emit that does not land is written locally and nowhere else.
-
-## What this needs that the mesh does not have
-
-Named here rather than assumed, because each is a decision of its own and this record is not the place to
-make them:
-
-1. **A module that every machine has.** `ScopeNode` means *at most one holder per node* — an exclusivity
- rule, not an obligation — and nothing assigns a module at enrolment. Today the resolver, the packet
- filter, ssh and intrusion prevention are each assigned per machine by hand, which is the same gap
- wearing different clothes.
-2. **A container running a module's own bundle.** A `process` runs the mesh's own compiled code with no
- image; a `container` needs an image of the module's own, which means a Dockerfile — the thing the
- `bundle` artifact exists to abolish. Nothing in the catalogue runs a bundle in a container, so
- `connect-docker` has no shape yet.
-3. **A unit a package ships, for `connect-unit`.** The `service` resource puts an existing unit into a
- state and deliberately installs none, so this form needs a package that serves a port — and naming a
- program the machine may not have is
- [issue 136](../04-ISSUES/136-a-module-may-name-a-program-the-machine-does-not-have/00-report.md).
-4. **A machine's public domain in the roster fact.** The fact carries each machine's name, mesh name,
- address and operator account. The public names cannot be composed without the domain.
-5. **Something that installs the mesh's own root on a machine.** This is
- [issue 129](../04-ISSUES/129-nothing-makes-a-machine-trust-the-meshs-authority/00-report.md), open
- since before any of this. Until it is closed, every internal name will fail certificate verification
- from every machine — correctly, because nothing can verify it. That is the checker working, and it is
- worth saying in advance so the first run is not read as the checker being broken.
-
-## Consequences
-
-- **A failure names the machine and the hosting form.** That is the whole gain over a port: eleven hours
- became two runs under 0145, and under this it also becomes one line that says where to look.
-- **The checker surfaces issue 129 immediately**, and will report every internal name unverifiable until
- it is fixed. A reader must be told that before the first run rather than after.
-- **Five things must be built before this is what it says it is**, and until they are, what exists is a
- port dial from one position — useful, and not this.
-- **What got harder:** a module with three hosting forms of the same trivial service is a strange thing to
- read. It is justified only because those three forms are how the mesh actually runs software, and a
- checker that tested one of them would keep the class of outage it exists to catch.
-
-## How it is checked
-
-- **A name per hosting form answers from every machine**, asserted by name and not by port.
-- **A certificate that does not verify is reported as that**, distinctly from a name that does not resolve
- and a port that does not answer — three faults, three owners.
-- **A machine that joins is checked by every other machine without an edit**, asserted by adding one to a
- bed and looking at what the others dial on their next run.
-- **A machine that joins has the module**, which is gap 1 above and is the assertion that cannot be
- written yet.
-- **The measured outage is still caught**: the path from a container to a service on its own machine is
- closed and `connect-docker..internal` fails from that machine while the others still pass.
-
-## References
-
-- [ADR 0145](0145-a-module-checks-what-the-mesh-claims-is-reachable.md) — superseded; its core stands
-- [ADR 0138](0138-an-assignment-binds-an-endpoint-and-says-how-far-it-reaches.md) — the two reaches these
- names come from
-- [ADR 0066](0066-public-routing-is-name-agnostic.md) — a label plus a domain, which is why no name is written
-- [issue 129](../04-ISSUES/129-nothing-makes-a-machine-trust-the-meshs-authority/00-report.md) — what the
- internal names will fail on until it is closed
-- [issue 145](../04-ISSUES/145-a-machine-reads-healthy-while-its-modules-cannot-reach-each-other/00-report.md)
diff --git a/02-DECISIONS/0147-a-module-anchors-the-meshs-authority.md b/02-DECISIONS/0147-a-module-anchors-the-meshs-authority.md
deleted file mode 100644
index a44aa3d1..00000000
--- a/02-DECISIONS/0147-a-module-anchors-the-meshs-authority.md
+++ /dev/null
@@ -1,169 +0,0 @@
----
-topic: what runs on it
-status: accepted
-date: 2026-09-29
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0098-a-fact-a-provider-makes-at-first-start-is-fetched-from-it.md
----
-
-# 147. A module anchors the mesh's authority on a machine, and takes it away again
-
-## Context
-
-The mesh runs its own certificate authority and every internal name is served with a certificate
-from it. No machine trusts it. On an enrolled, adopted workstation — on the private network,
-resolving through the mesh's resolver — every internal HTTPS name fails verification with
-*unable to get local issuer certificate*
-([issue 129](../04-ISSUES/129-nothing-makes-a-machine-trust-the-meshs-authority/00-report.md)).
-The certificates are genuine; nothing on the machine has ever been told what issued them.
-
-The authority's only consumer today is a proxy, which fetches the root into a directory of its own
-and hands it to one program ([ADR 0098](0098-a-fact-a-provider-makes-at-first-start-is-fetched-from-it.md)).
-That is enough for the proxy and for nothing else: a browser, `git` over HTTPS, `curl`, a package
-manager and every module that calls another module by an internal name read the machine's trust
-store, which holds the predecessor's authority and a developer tool's local root, and nothing of
-the mesh's.
-
-The predecessor wrote its root into every machine it set up. Removing it was deliberate — an
-honest failure beats a name that verifies for the wrong reason — and it leaves the mesh with no
-answer at all until this one lands. It is also what keeps the predecessor alive on the machines
-that still speak TLS to a mesh name.
-
-**What makes this a decision rather than a patch** is where the knowledge goes. Two mechanisms in
-the mesh already write things onto a machine because it is on the private network: `/etc/hosts`
-and the registry's plaintext trust ([ADR 0082](0082-the-registry-is-reached-by-name-and-trusted-by-the-overlay.md)).
-Following that precedent, the controller would inject an anchor into every such machine's
-declaration, and issue 129 proposed exactly that. It would work. It would also put *where this
-operating system keeps trust anchors* and *which command refreshes its bundles* into the control
-plane, for a fact the control plane does not have (the root does not exist until the authority has
-run) and a machine that may have no reason to verify a mesh name at all.
-
-## Considered Options
-
-1. **The controller injects the anchor into every machine on the private network**, the
- `/etc/hosts` and insecure-registry shape. Rejected: being on the network is what makes the
- registry reachable, and that is why network presence is the right trigger *there* — the trust
- and the reachability are the same fact. Trusting an authority is not the same fact as being
- able to reach it, and the anchor's path and the bundle refresh are a property of the machine's
- operating system, which is the host's half of the mesh, not the controller's.
-2. **A new host primitive — a `trust-anchor` resource type.** Rejected for now, not on principle.
- The host's vocabulary should grow when a shape cannot be said with what exists, and this one
- can: a file and a service already express it, as the packet filter proves
- ([ADR 0140](0140-the-filter-constrains-what-arrives-from-outside.md), whose module writes a
- unit file and a service and nothing else). The primitive becomes right the moment a second
- operating system is in play, because the anchor directory and the refresh command are exactly
- the difference `internal/system` exists to hold. Until then it would be a vocabulary word with
- one speaker.
-3. **A module that requires the authority, fetches its root, installs it as an anchor and
- refreshes the machine's bundles — and removes both when it is no longer assigned.** Adopted.
-
-## Decision
-
-**A machine trusts the mesh's authority because a module put its root there, and stops trusting it
-when that module is taken away.**
-
-1. **The module requires `internal-acme-ca`** and reads the provider's bound address and the path
- it serves its root at. It requires nothing else and provides nothing: it is a consumer of the
- authority like any other.
-2. **It fetches the root over the mesh's own network, without prior trust**, because there is no
- prior trust to have — this is the module that establishes it — and the network is what
- authenticates the fetch ([ADR 0098](0098-a-fact-a-provider-makes-at-first-start-is-fetched-from-it.md),
- the same reasoning that lets the proxy fetch it). What it accepts is checked: a body that is
- not a certificate fails, and the failure is the module's, not a later handshake's.
-3. **It installs the root where this machine's TLS clients look, and refreshes the extracted
- bundles** — the command that does the refresh is an ordinary part of the unit that places the
- anchor, not a new thing the mesh can be asked to do.
-4. **Removal is symmetric and is the same unit's business.** Undeclared, the host stops the unit;
- stopping it removes the anchor and refreshes the bundles again. A machine that leaves the mesh
- stops trusting the mesh, without anybody remembering to go and look.
-5. **It is an ordinary assignment.** No machine is given it automatically. A machine that verifies
- a mesh name is assigned it, and a machine that does not is not — which is the same statement
- the mesh already makes about every other module, and is why this is not the controller's
- business.
-
-**One operating system, said out loud.** The anchor directory and the refresh command in the
-module today are Arch's. On a machine that is not Arch the unit fails, visibly, rather than
-writing a file nothing reads. That is the accurate failure, and it is the signal that option 2
-above has become right.
-
-## How this is checked
-
-- **The verification that could not succeed before.** On a machine holding the module, a plain
- client fetches an internal HTTPS name with no `-k` and no bundle argument and verifies. On a
- machine without it, the same fetch fails with *unable to get local issuer certificate*. Both
- halves, because only the pair distinguishes "the anchor works" from "something else already
- trusted it".
-- **The removal half, in the same bed:** unassign the module, refetch, and the failure returns.
- Checking only the arrival is how a trust store fills up with authorities nobody can account for.
-- **What is deliberately not checked here:** that the authority issues, that a name resolves, that
- the proxy serves. Those have their own beds, and this module's bed passing for those reasons is
- the failure mode this record is most exposed to — which is why the negative half is not optional.
-
-**What this bed is dialled at, and why it is the authority itself.** The authority serves its own
-API with a certificate it issued, so the handshake under test needs nothing else in the mesh to be
-right. A trust bed that reached for a routed name through the proxy would be passing or failing for
-the proxy's reasons and the resolver's.
-
-**Written, and not yet run** *(2026-09-29)*. The bed is `trust-anchor` in the lab, and it cannot
-execute: raising a foundation fails before any module is reached, in both bundles that exist
-([issue 146](../04-ISSUES/146-the-foundation-cannot-be-raised-on-the-bus-the-mesh-runs-on/00-report.md)).
-So what stands behind this record today is the rendering — the script the machine would run names
-the authority it was bound to, checked in the control plane's own test suite — and **not** a machine
-that verified anything. That is a weaker thing than the paragraph above describes, and it stays
-written this way until the bed runs.
-
-> **Progressive insight — 2026-09-30. It has now been run, on the live mesh rather than in the bed.**
-> The paragraph above said nothing had verified anything, and something has. The module was registered
-> from the catalogue, assigned to a workstation, and checked in the form this section prescribes — the
-> authority's own API, so the handshake needs nothing else in the mesh to be right:
->
-> ```
-> $ curl -sS -o /dev/null -w '%{http_code}' https://:9000/health
-> 200
-> subject=CN=Step Online CA
-> issuer=O=Mesh Internal CA, CN=Mesh Internal CA Intermediate CA
-> Verify return code: 0 (ok)
-> ```
->
-> **Both halves.** Unassigning and pushing removed the anchor, emptied the trust store of the mesh's
-> authority, and returned the plain client to *unable to get local issuer certificate* — then assigning
-> again restored it. The negative half is what distinguishes the anchor working from something else
-> having trusted it, and it is the half nothing had ever exercised.
->
-> **One thing this found that is not in the module.** The removal only works because the *host* removes
-> the service before the script: stopping the unit is what deletes the certificate and refreshes the
-> bundles, and it needs the script it calls to still exist. Nothing in the module states that ordering;
-> the symmetry this record claims rests on it.
->
-> Run on the live mesh because that is where a change is verified now
-> ([ADR 0149](0149-the-live-mesh-is-the-test-bed.md)), and the bed still cannot raise a foundation. The
-> evidence is [issue 129](../04-ISSUES/129-nothing-makes-a-machine-trust-the-meshs-authority/02-resolution.md).
-> Extended the same day to every converged machine — `novox`, `g14` and `shanks` each hold the anchor
-> and verify with a plain client. `ace` is excluded on purpose: it is adopted, so a module assigned
-> there is held rather than run, which is right and is not trust.
-
-## Consequences
-
-The predecessor's authority can be retired from a machine once this module is assigned to it,
-which is the first time that has been true. `git` over HTTPS to the mesh's forge starts working,
-so the ssh-only clone URL stops being a rule. A module on any machine can call another module's
-internal name and verify it.
-
-What got harder: one more module to assign to a machine that needs it, and the machine's trust
-store now changes when an assignment changes — which is the point, and is also a thing an operator
-can be surprised by. The fetch without prior trust is the same exposure ADR 0098 accepted, now on
-every machine that holds the module rather than only where a proxy runs: anything that can stand
-in the middle of the mesh's own network at the moment of the fetch can be believed. The mesh
-already treats that network as the thing it authenticates.
-
-## References
-
-- [issue 129](../04-ISSUES/129-nothing-makes-a-machine-trust-the-meshs-authority/00-report.md) —
- the symptom and the evidence.
-- [ADR 0098](0098-a-fact-a-provider-makes-at-first-start-is-fetched-from-it.md) — a fact made at
- first start is fetched from its provider; this extends it from one program to the machine.
-- [ADR 0082](0082-the-registry-is-reached-by-name-and-trusted-by-the-overlay.md) — the precedent
- this deliberately does not follow, and why it is right where it is.
-- [ADR 0005](0005-the-node-host.md) — the host is where one operating system's difference lives.
-- [`03-DESIGN/01-to-be/08-connectivity.md`](../03-DESIGN/01-to-be/08-connectivity.md).
diff --git a/02-DECISIONS/0148-the-meshs-names-are-resolved-not-copied-into-containers.md b/02-DECISIONS/0148-the-meshs-names-are-resolved-not-copied-into-containers.md
deleted file mode 100644
index 11dbfb2f..00000000
--- a/02-DECISIONS/0148-the-meshs-names-are-resolved-not-copied-into-containers.md
+++ /dev/null
@@ -1,176 +0,0 @@
----
-topic: the tiers
-status: accepted
-date: 2026-09-30
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0066-public-routing-is-name-agnostic.md
----
-
-# 148. The mesh's names are resolved, not copied into every container
-
-## Context
-
-The mesh gives every container it declares the whole roster of mesh names as entries written into
-the container's own hosts file at creation
-([design 08 §2](../03-DESIGN/01-to-be/08-connectivity.md),
-[ADR 0066](0066-public-routing-is-name-agnostic.md)). A container takes those entries once and never
-looks again.
-
-Three issues are the same fact arriving three times.
-
-**A container keeps the address it was made with.** Adopting the predecessor's tunnel moved the hub's
-private address; the declaration followed it within one push and nothing on the machine did. The
-forge's container held the old address, lost its database, reported healthy while its existing
-connections lasted, and then the public name went down
-([issue 109](../04-ISSUES/109-a-container-keeps-the-address-it-was-made-with/00-report.md)).
-
-**The same fault, four days later, undetected for five days.** One container had restarted 2286 times
-against a database it could no longer find, while the mesh reported the machine as doing what it was
-told. Inside it, `novox.internal` was an address that had not existed for five days
-([issue 135](../04-ISSUES/135-a-containers-mesh-names-are-not-compared/00-report.md)). Forty-eight
-other containers were current, none of them corrected — each had been recreated for some other
-reason and picked up the roster on the way.
-
-135 was fixed by putting the roster into the digest the host compares a container against, so a
-container whose names moved is recreated like one whose image moved. **That made the roster part of
-every container's identity**, which is the third arrival:
-
-**One name moving replaces every container in the mesh.** Migrating one small module on one machine
-took four routine actions; each changed the roster, and each replaced every container on the control
-node — its own store, the registry, the edge proxy, the forge, the directory, mail. The control plane
-was unreachable twice while its own store came back through crash recovery
-([issue 151](../04-ISSUES/151-a-new-name-recreates-every-container-in-the-mesh/00-report.md)). None of
-the replaced containers had anything to do with the module being migrated, or with its machine.
-
-The blast radius of a name is now every container that carries the list, which is all of them. The
-node-by-node migration ahead adds names one module at a time — on one machine alone that is around
-twenty-five — and each would be a full restart of every service on the hub.
-
-## Considered Options
-
-**1. Keep the roster in every container and accept the churn.** Rejected. It is not a cost that can
-be paid down: the mesh gets more names as it grows, and every name costs a restart of everything.
-A rollback costs another.
-
-**2. Scope each container's entries to the names it actually binds.** A container is given the names
-of the things it declared a requirement on, so a name's blast radius is its consumers. Tidy, needs no
-new mechanism, and keeps 135's guarantee exactly.
-
-Rejected, and this is the close one. It contradicts the standing intent that **anything on the mesh
-can call anything on it** — three cases, same machine, the private network, the public network, and
-no fourth. Scoping resolution to declared couplings makes a name reachable only where the mesh was
-told in advance that it would be wanted, and a person debugging inside a container would find names
-missing that exist everywhere else on the machine. It also leaves the roster in the digest, so the
-churn returns the moment a widely-bound name moves — smaller, not gone.
-
-**3. Resolve at lookup time through the machine's resolver, and copy nothing.** Chosen.
-
-## Decision
-
-**A container resolves the mesh's names through its machine's resolver, at the moment it asks. No
-mesh name and no mesh address is written into a container, and none is part of a container's
-identity.**
-
-The three consequences that make this worth doing:
-
-- **Staleness stops being possible**, rather than being detected. 109 and 135 are not bugs that were
- fixed; they are a shape that no longer exists. A name that moves is answered differently by the next
- lookup, in every container, with nothing recreated and nothing restarted.
-- **A name's blast radius becomes nothing.** Assigning a module on one machine does not touch a
- container on another.
-- **Anything can still call anything**, which option 2 gave up. The resolver answers every mesh name to
- every asker on the machine, exactly as it answers the machine itself.
-
-**The resolver is a machine-level process, not a container** — one of the modules that is not a
-container at all — so a container depending on it is not the circularity it would be if the mesh's
-own store had to resolve a name through something the store's own runtime had to start first.
-
-**What a module declares for itself is untouched.** Entries a manifest asks for are the module's own,
-stay in the container, and stay in its identity: they are part of what the module *is*, they do not
-move when the mesh's roster does, and the mesh does not know what they mean.
-
-**The machine's own roster file is untouched.** It is a file, rewritten in place, read by processes and
-people; nothing restarts when it changes. It is only the *copy into each container* that this ends.
-
-### The order this lands in, which is not a preference
-
-**Nothing may stop copying names until resolution works from a container.** Removing the copy first
-reintroduces 109 and 135 — silently, and on a live mesh, which is exactly how both were found.
-
-1. **A container on any network can reach the resolver.** Today a container on the runtime's default
- network asks from an address the converged filter drops, so it has no DNS at all
- ([issue 110](../04-ISSUES/110-a-container-on-the-runtimes-own-network-cannot-reach-the-resolver/00-report.md));
- and on two of four machines the resolver binds loopback only, so the runtime hands containers a
- public resolver instead. Both are prerequisites, not related work.
-
- > **Progressive insight — 2026-09-30, later the same day. The loopback claim was wrong.** The
- > resolver bound the private address on all four machines; on two the runtime had never been told
- > to use it, and on all four the resolver discarded a query that arrived on the runtime's bridge.
- > The step stands; the facts under it were those. Both fixed the same day
- > ([issue 110's resolution](../04-ISSUES/110-a-container-on-the-runtimes-own-network-cannot-reach-the-resolver/01-resolution.md)),
- > and step 3 landed after them.
-2. **The runtime is told which resolver to use, per machine, as a file** — not per container as a
- creation-time argument, or the resolver's address is back in every container's identity and the
- problem has only got smaller.
-3. **Then, and only then, the roster leaves the declaration and the digest.**
-
-Until step 3 the mesh keeps copying, and keeps comparing. 151 stays open until step 3 lands; it is not
-closed by this record, only answered by it.
-
-## How this is checked
-
-- **A container resolves a name that moved, without being recreated.** Move a name the mesh serves;
- from a container that was running before the move and has not been touched since, the name answers
- with the new address. This is the one 109 and 135 would both have failed.
-- **A name's blast radius is nothing.** Add a routed name on one machine; no container on any other
- machine is recreated. The apply report on each machine says nothing changed. This is 151.
-- **Anything calls anything.** From a container on any machine, every `.internal` name and every
- routed name the mesh serves resolves — including names the module never declared a requirement on,
- which is the guarantee option 2 would have given up.
-- **On every network the runtime offers.** The first three hold for a container on the runtime's
- default network as well as one on a declared network, because the default network is the case that
- has no DNS today.
-- **No mesh name is in a container's spec.** A test asserts the digest a host computes for a container
- does not move when the mesh's roster does, and does move when the module's own declared entries do.
-
-## Consequences
-
-- **The resolver becomes load-bearing for every container**, where before it was load-bearing for the
- machine. This is a real cost and is accepted: a resolver that is down is a machine that cannot
- resolve, which is already true of the machine itself, and is a smaller event than a roster change
- destroying and recreating every container on the machine.
-- **Design 08's "a file rather than a resolver" no longer describes containers.** It was written when
- the mesh had no resolver and it gave the right answer then. The reasoning it rested on — every Linux
- has a hosts file, no package needed — was already overtaken by names a hosts file cannot express:
- service names and wildcards under `.internal`, which is why the resolver was built.
-- **ADR 0066's mesh-wide propagation is kept and its mechanism changes.** A routed name still reaches
- every asker in the mesh; it reaches them through the resolver rather than by being written into each
- container. The consequence 0066 records — that an internal issuer's challenge needs the routed name
- resolvable inside the mesh — holds unchanged and by the same means the machine already uses.
-- **Issue 110 stops being a container-DNS inconvenience and becomes a prerequisite** for the mesh not
- restarting itself whenever it learns a name.
-- **A container that names a resolver of its own has opted out of the machine's**, and the copy this
- record removes was the only reason such a container could reach anything by a mesh name.
-
- > **Progressive insight — 2026-09-30, the afternoon this landed. Found the hard way.** The mail
- > system's admin, behind Mailu's own resolver, lost its database the moment the copy went
- > ([issue 171](../04-ISSUES/171-a-modules-own-resolver-knows-no-mesh-name/00-report.md)). A `dns` on
- > a container is a decision about whether mesh names exist inside it, not a preference; the module
- > was corrected, and whether the controller should refuse the contradiction is that issue's open
- > question.
-
-- **A container started by hand gets the mesh's names too**, where before only declared containers did.
- Design 08 drew that boundary deliberately, on the grounds that reaching into every container is what
- a nameserver would be for. This record accepts that consequence rather than working around it: a
- person debugging in a hand-started container resolving the same names as everything else is the
- behaviour worth having, and it is what "anything can call anything" means.
-
-## References
-
-- [issue 151](../04-ISSUES/151-a-new-name-recreates-every-container-in-the-mesh/00-report.md) — one name replaces every container; the question this answers
-- [issue 135](../04-ISSUES/135-a-containers-mesh-names-are-not-compared/00-report.md) — the roster put into the digest
-- [issue 109](../04-ISSUES/109-a-container-keeps-the-address-it-was-made-with/00-report.md) — the first arrival
-- [issue 110](../04-ISSUES/110-a-container-on-the-runtimes-own-network-cannot-reach-the-resolver/00-report.md) — the prerequisite
-- [ADR 0066](0066-public-routing-is-name-agnostic.md) — routed names propagate mesh-wide; extended here
-- [design 08 §2](../03-DESIGN/01-to-be/08-connectivity.md) — the file-not-resolver reasoning this narrows
diff --git a/02-DECISIONS/0149-the-live-mesh-is-the-test-bed.md b/02-DECISIONS/0149-the-live-mesh-is-the-test-bed.md
deleted file mode 100644
index cdb538af..00000000
--- a/02-DECISIONS/0149-the-live-mesh-is-the-test-bed.md
+++ /dev/null
@@ -1,106 +0,0 @@
----
-topic: building it
-status: accepted
-date: 2026-09-30
-deciders: jochen
-reconstructed: false
-supersedes: 02-DECISIONS/0068-the-lab-takes-requests.md
----
-
-# 149. The live mesh is the test bed
-
-## Context
-
-[ADR 0068](0068-the-lab-takes-requests.md) proposed that the lab accept queued requests
-— a bed and a commit — answer them one at a time from a copy it owns, and expose that through tools so
-an agent could start a run and come back to it. It has been `proposed` since 2026-09-12 and nothing was
-built.
-
-What happened instead is that the mesh became the thing under test. It runs on four machines; every
-fault worth finding in the last month was found on them, and none was found in a bed:
-
-- a container holding an address that had not existed for five days, on the control node
- ([issue 135](../04-ISSUES/135-a-containers-mesh-names-are-not-compared/00-report.md));
-- a machine reading healthy for eleven hours while no module could reach another
- ([issue 145](../04-ISSUES/145-a-machine-reads-healthy-while-its-modules-cannot-reach-each-other/00-report.md));
-- one name replacing every container on the hub
- ([issue 151](../04-ISSUES/151-a-new-name-recreates-every-container-in-the-mesh/00-report.md));
-- a consumer assertion that is correct on a mesh being raised and fatal on one that is running
- ([issue 156](../04-ISSUES/156-moving-a-consumers-delivery-subject-stops-the-control-plane/00-report.md)).
-
-The last is the one that settles it. That change was exercised on the raise path — which is what a bed
-*is* — and the raise path is the only path on which the fault cannot appear. A bed raises a mesh; it
-does not have a mesh that has been running for weeks, with consumers already bound, containers created
-against an older roster, and an adopted machine carrying a predecessor's configuration. **The faults
-that cost the most were all faults of a mesh that already exists**, and a bed is by construction a mesh
-that does not.
-
-Lab runs are also expensive in a way that changed the behaviour around them: each costs a build and
-several minutes, so they were batched, and a batched test is one whose result arrives after the next
-three changes were already written.
-
-## Considered Options
-
-**1. Build 0068 as proposed.** Rejected. It answers a question nobody is asking: the bottleneck was
-never that a person had to sit at the lab, it was that a bed cannot hold the state the faults live in.
-Queueing and tooling a mechanism that finds the wrong class of fault faster is not an improvement.
-
-**2. Leave 0068 `proposed`.** Rejected, and it is why this record exists rather than nothing. A record
-that contradicts current practice and sits unresolved is worse than either answer: it reads as intent
-to anyone who finds it, and the practice it contradicts is written down nowhere but a handoff note.
-
-**3. Record that the live mesh is the test bed, and supersede 0068.** Chosen.
-
-## Decision
-
-**A change is verified against the mesh that is running.** Not because a bed would be unwelcome, but
-because the state that breaks things is state a bed does not have: containers made against an older
-roster, consumers already bound, an adopted machine, a store with weeks of history.
-
-**A change that can only be exercised on the raise path is not verified.** If the only test available
-raises a fresh mesh, the record says so, and says which case was therefore not covered. The words
-"exercised on a fresh mesh" are a statement about coverage, not a pass.
-
-**The lab is not retired**, and [ADR 0016](0016-the-lab.md) stands. It remains the place to raise a
-mesh from bare, which is the one thing the live mesh cannot be asked to do and the one thing a bed does
-better than anything else. What this record removes is the lab as the *default* answer to "is this
-change good", and with it 0068's queue, tools and request protocol.
-
-**Accuracy over a green run.** An honest failure on the live mesh beats a pass in a bed that could not
-have failed — and a change that is risky on the running mesh is a reason to make the change smaller,
-not a reason to test it somewhere it cannot break.
-
-**What 0068 got right is kept as a rule, not a mechanism:** a run reads a copy that is not anybody's
-working tree. Every run of the lab that mattered was pinned to a checkout rather than a worktree, and
-the ones that were not produced results about code nobody had written down.
-
-## How this is checked
-
-- **A record that says a change was verified says on what.** Where it was a fresh mesh, it says which
- case is uncovered. This is the clause that would have caught 156: its change was verified, honestly,
- on the only path where it works.
-- **The lab is not in the path of a merge.** No check, playbook or handoff requires a bed to have run.
-- **0068 is unreachable as intent.** Its status is `superseded` and it names this record, so a reader
- arriving at the queue design finds out immediately that it was not built and why.
-
-## Consequences
-
-- **A fault can be introduced on the machines that serve.** This is the cost, it is real, and it was
- paid twice in one evening — a control plane crash-looping for half an hour, and every container on the
- hub recreated five times. Both were found in minutes because they were live, and both would have
- passed a bed.
-- **There is no pre-merge gate beyond the repositories' own suites.** `make check` and the three hq
- checks are what stands between a change and the machines, which raises what those suites are worth
- and makes a test that cannot fail a genuine defect rather than an untidiness.
-- **Raising a mesh from bare is now the lab's whole job**, and is exercised deliberately rather than
- as a side effect of testing something else. The foundation work
- ([issue 146](../04-ISSUES/146-the-foundation-cannot-be-raised-on-the-bus-the-mesh-runs-on/00-report.md))
- is that job, and it is also the proof that the mesh can make another of itself.
-- **An agent cannot hand a run to a queue and come back**, which 0068 would have given. In practice it
- watches a push and reads the machines, which is what happened anyway.
-
-## References
-
-- [ADR 0068](0068-the-lab-takes-requests.md) — superseded by this
-- [ADR 0016](0016-the-lab.md) — the lab, which stands
-- [issue 156](../04-ISSUES/156-moving-a-consumers-delivery-subject-stops-the-control-plane/00-report.md) — correct on the raise path, fatal on a running mesh
diff --git a/02-DECISIONS/0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md b/02-DECISIONS/0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md
deleted file mode 100644
index d526b699..00000000
--- a/02-DECISIONS/0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md
+++ /dev/null
@@ -1,117 +0,0 @@
----
-topic: what runs on it
-status: accepted
-date: 2026-09-30
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0047-a-module-runs-its-code-as-its-own-process-with-its-own-account.md
----
-
-# 150. A module's own code runs as supervised processes under the module's one account
-
-> **Widened — 2026-10-02, by [ADR 0188](0188-a-modules-own-code-is-bundles-in-any-language-and-a-tools-bundle-speaks-mcp-to-the-runtime.md).** A module's long-lived process is a bundle in any language the mesh has a toolchain for, run as a unit the host writes; this record never said one language and never meant one, and 0188 says so as the rule.
-
-> **The mechanism changed — 2026-10-02, by [ADR 0175](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md).** For a module's *tools*, read that record: one runtime per node, the node's one account, bundles loaded from the memberships. This record still governs a module's long-lived processes — a daemon, a provisioner, a scheduled ingest — and the account invariant for them.
-
-## Context
-
-The repository answers "what runs a module's own code" two ways and reconciles them nowhere
-([issue 117](../04-ISSUES/117-a-modules-own-code-is-a-container-and-a-process/00-report.md)).
-
-[ADR 0047](0047-a-module-runs-its-code-as-its-own-process-with-its-own-account.md) is accepted and
-says **a container** — "the tool runtime carrying that module's compiled code" — and "one module, one
-process, one account". Two `proposed` design documents say a **`process`** resource running an argv,
-supervised by the machine, and one of them declares *four* of them for a single module and presents
-four as the point. Neither design document names 0047 in its `decisions:`, and the string `process`
-as a resource type appears in no decision record at all. The thing as built is the container.
-
-Two things have happened since 0047 was written that bear on it directly.
-
-[**ADR 0142**](0142-the-mesh-delivers-its-own-components-as-binaries.md) decided that the mesh's own
-components are binaries on the machine rather than container images, and
-[issue 114](../04-ISSUES/114-should-the-controller-be-a-container-or-a-process/00-report.md) was closed
-by it. That settled the mesh's components and deliberately said nothing about a module's.
-
-And the standing definition of a module hardened: **a module is software that delivers one or more
-services, and a module is not a container.** It may deliver them as a container, an installed package
-with a unit, a binary, or configuration files; 61 of 73 happen to use a container and 11 do not,
-including the resolver, sshd and fail2ban. A rule that a module's *own code* must be a container makes
-the one kind of module the mesh writes itself the only kind that has no choice.
-
-## Considered Options
-
-**1. Hold 0047 as written: a container.** Rejected. Its own reasoning does not require one. What 0047
-argued for was a runtime **per module** rather than one for the whole node, because a node-wide runtime
-could not hold a per-module broker account and per-module runtimes competing on one tool key would each
-be handed calls for tools they do not have. A supervised unit per module satisfies that argument
-exactly — it is per module, and a unit runs as an account. The container was the mechanism to hand, not
-the conclusion.
-
-**2. Let each design document choose.** Rejected; that is the present state and it is what issue 117
-reports. A module author reading the guide writes four processes; a module author reading the record
-writes a container; nothing tells either that the other exists.
-
-**3. Settle the hosting form as a supervised process, and settle the count separately.** Chosen.
-
-## Decision
-
-**A module's own code runs as one or more supervised processes on the machine, under the module's single
-account.** Where [ADR 0047](0047-a-module-runs-its-code-as-its-own-process-with-its-own-account.md) says
-"a container, the tool runtime carrying that module's compiled code", read this record. Everything else
-0047 decided stands untouched: a tool is served on its own key, only the module that serves it answers,
-and the module's account is scoped to exactly its tool keys.
-
-**The invariant is the account, not the process count.** 0047's "one module, one process, one account"
-carried its weight in the last clause. Its stated worry about a second process was "not a second one to
-scope and seal" — a second *identity* to grant, seal a secret to, and scope on the bus. Several
-processes sharing the module's one account create no second identity, so nothing further is scoped or
-sealed, and a module may therefore declare as many as its work has shapes: events, tools, a
-provisioner, a scheduled ingest. **What a module may not have is two accounts.**
-
-**A module that delivers its service as a container still does.** This record is about the code the
-module itself carries — its tools, its events, its provisioner — and not about the software it delivers.
-A module wrapping a third-party image wraps a third-party image.
-
-**Why supervised by the machine rather than by the mesh:** it is the same answer ADR 0142 gave for the
-mesh's own components, for the same reason. A unit the machine restarts needs no image, no registry
-pull and no runtime to be up before the mesh's own code can run — which matters most for exactly the
-modules whose code the mesh cannot start any other way.
-
-## How this is checked
-
-- **No design document describes a hosting form for a module's own code without citing this record.**
- Designs 18 and 20 name it in `decisions:`; this is the gap issue 117's third point reports, and
- `cycle.py` already enforces that a to-be design names its decisions.
-- **A module declaring several processes resolves to one account.** A test composes a module with more
- than one process resource and asserts the mesh mints exactly one broker account for it, scoped to that
- module's tool keys and nothing else — which is 0047's invariant stated as an assertion rather than a
- sentence.
-- **A module's own code does not require the container runtime.** A machine with no container runtime
- can still run a module whose code is its own, which is the claim that separates this from option 1 and
- is checkable on a machine that has one by asserting the declaration names no image for it.
-
-## Consequences
-
-- **The sidecar port stops being needed.** [ADR 0029](0029-a-network-is-a-shape-because-an-action-cannot-be-undone.md)
- records that "anything that is a service plus a sidecar currently has to publish a port to talk to
- itself", and the host's `network` shape exists partly for it. A process beside the service on the same
- machine reaches it without publishing anything, so that pressure goes.
-- **Something must supervise, and it is the machine.** This adds a unit per module's code to what the
- host writes and owns. The mesh already writes and owns units — `nftables` proves a module can write one
- and run it — so the mechanism exists; the count grows.
-- **A module's code is delivered, not pulled**, which puts it behind the same gap as the host's own
- delivery ([ADR 0141](0141-the-host-delivers-its-own-successor.md), not built): nothing yet delivers a
- version of a module's binary to a machine. A container's code arrives by `docker pull`, and this does
- not. **This is the cost of the decision and it is not paid**; until delivery exists, a module whose code
- is its own is a module somebody places by hand.
-- **Issue 117 is answered and its three disagreements close differently:** container-or-unit is decided
- here; one-process-or-several is decided here as several under one account; and whether the record was
- consulted is fixed by designs 18 and 20 naming this one.
-
-## References
-
-- [issue 117](../04-ISSUES/117-a-modules-own-code-is-a-container-and-a-process/00-report.md) — the contradiction this answers
-- [ADR 0047](0047-a-module-runs-its-code-as-its-own-process-with-its-own-account.md) — extended; its "a container" clause is settled here
-- [ADR 0142](0142-the-mesh-delivers-its-own-components-as-binaries.md) — the same answer for the mesh's own components
-- [ADR 0141](0141-the-host-delivers-its-own-successor.md) — the delivery this depends on and which is not built
-- [ADR 0029](0029-a-network-is-a-shape-because-an-action-cannot-be-undone.md) — the sidecar port this relieves
diff --git a/02-DECISIONS/0151-a-routes-internal-name-is-composed-under-the-node-that-serves-it.md b/02-DECISIONS/0151-a-routes-internal-name-is-composed-under-the-node-that-serves-it.md
deleted file mode 100644
index 7339440a..00000000
--- a/02-DECISIONS/0151-a-routes-internal-name-is-composed-under-the-node-that-serves-it.md
+++ /dev/null
@@ -1,104 +0,0 @@
----
-topic: the tiers
-status: accepted
-date: 2026-09-30
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0066-public-routing-is-name-agnostic.md
----
-
-# 151. A route's internal name is composed under the node that serves it
-
-> **Superseded in part — 2026-10-03, by [ADR 0191](0191-the-meshs-resolver-holds-only-the-meshs-own-names.md).**
-> *"The roster publishes it as itself, once, at the serving node's address"* no longer holds: a public name
-> is never given a private answer, and resolves publicly. The internal name this record
-> composes is what that rests on, and stands.
-
-## Context
-
-A module that requires a route is given two names from one label: a public one, `.`, and an internal one, `..internal`
-([ADR 0138](0138-an-assignment-binds-an-endpoint-and-says-how-far-it-reaches.md)). Both were composed
-from the node the module runs on.
-
-The two are answered differently. The public name is published into every machine's roster at the
-address of the node whose proxy serves it ([ADR 0066](0066-public-routing-is-name-agnostic.md)), so
-it reaches the proxy from anywhere in the mesh. The internal name is answered by every machine's
-resolver as *anything under a node's name goes to that node*
-([design 08 §2](../03-DESIGN/01-to-be/08-connectivity.md)) — the node it was composed from, which is
-the consumer's. Where the proxy runs on another machine, that name sends a client to a machine with
-nothing listening, while the public name works
-([issue 139](../04-ISSUES/139-an-internal-route-name-resolves-to-the-consumers-node/00-report.md)).
-Every route on this mesh today is served beside its module, so it has not been seen; `route` is
-provided mesh-wide precisely so that stops being true.
-
-Beside it, the roster gave every routed name a second entry with the mesh's suffix appended —
-`..internal` — because it composed a full name for every entry as it does for a
-machine. That name resolved on every machine, was served by nothing, and was refused by the proxy at
-the handshake; the first three names tried while reproducing an unrelated issue were those, and the
-evidence pointed at a regression that had not happened
-([issue 157](../04-ISSUES/157-a-routed-names-internal-alias-is-served-by-nothing/00-report.md)).
-
-## Considered Options
-
-**1. Keep the consumer's name and publish it at the serving node's address**, as the public name is.
-The name stays `..internal` and an exact roster entry overrides the wildcard.
-Rejected: it makes `..internal` mean *goes to that node* except when it does not, which is
-the one rule the resolver design states; it needs an entry per route where the wildcard needed none;
-and which of an exact entry and a wildcard a resolver answers first is the resolver's business, which
-the mesh deliberately does not know.
-
-**2. A proxy on every machine, so the serving node is always the consumer's.** Rejected for this
-question: it is a different decision about what `route` is — a node-scoped seat with a mesh-wide
-fallback — and this mesh runs one proxy on the hub today. Whatever is decided there, a route served
-from another machine must have a name that reaches it.
-
-**3. Compose the internal name under the node that serves the route.** Chosen.
-
-## Decision
-
-**A route's internal name is `..internal` — composed under the node whose proxy
-answers the route, which is the machine the request arrives at.** The public name is unchanged:
-`.` of the node the module runs on, which is where the operator put it.
-
-Where the proxy runs beside the module — every route on this mesh today — the two nodes are one and
-nothing changes. Where it does not, the name says where the request goes, which is what a name under
-a node's name has always meant.
-
-**A routed name has no mesh form.** The roster publishes it as itself, once, at the serving node's
-address. Only a machine has a bare name beside its full one.
-
-What certifies the internal name is unchanged by this: the proxy that terminates it obtains a
-certificate from the mesh's authority for the names it is given, and it is given this one.
-
-Taken on the operator's standing instruction to answer the open design questions in the work order.
-
-## How this is checked
-
-- **Composition.** A controller test contributes a route from a module on one node to a proxy offered
- from another, gathered the way the controller gathers a consumer's contribution for a provider on
- another machine, and asserts the internal name carries the serving node.
-- **Publication.** A controller test renders a roster with a machine and a routed name and asserts
- the routed name appears as itself, once, and never with the suffix appended.
-- **On the mesh.** After the change no machine's roster carries a `.internal` entry, and a
- route's internal name still answers from a container with a certificate from the mesh's authority.
-
-## Consequences
-
-- **A route served from another machine now has a usable internal name.** The first module assigned
- that way will resolve, where before it would have resolved to the wrong machine with no error.
-- **The internal name of a route can change when its proxy moves.** A route re-homed from one proxy
- to another gets a new internal name, as the design's rule implies; clients that dialled the old one
- reach the old machine. The public name does not move with the proxy and is the stable one.
-- **The roster is one line shorter per routed name**, and a person reading a hosts file no longer
- finds names that resolve to a refusal.
-- **Issue 139's second question — a per-node route holder — is left open**, and is a decision about
- what a seat is rather than about a name.
-
-## References
-
-- [issue 139](../04-ISSUES/139-an-internal-route-name-resolves-to-the-consumers-node/00-report.md) — the question
-- [issue 157](../04-ISSUES/157-a-routed-names-internal-alias-is-served-by-nothing/00-report.md) — the alias
-- [ADR 0066](0066-public-routing-is-name-agnostic.md) — routed names propagate mesh-wide; extended here
-- [ADR 0138](0138-an-assignment-binds-an-endpoint-and-says-how-far-it-reaches.md) — how the two names are composed and how far each reaches
-- [design 08 §2](../03-DESIGN/01-to-be/08-connectivity.md) — anything under a node's name goes to that node
diff --git a/02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md b/02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md
deleted file mode 100644
index eaf059b3..00000000
--- a/02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md
+++ /dev/null
@@ -1,162 +0,0 @@
----
-topic: what runs on it
-status: accepted
-date: 2026-09-30
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0095-the-control-plane-is-the-way-to-ask-a-module.md
----
-
-# 152. The operator's surface is a module the mesh assigns: the console
-
-## Context
-
-**Since the bus moved, nobody can ask the mesh anything without opening a shell on a machine.** Every
-tool call an operator's assistant makes fails, on every machine including the one the operator sits
-at, with *AMQP not connected*
-([issue 147](../04-ISSUES/147-the-operators-tools-still-dial-the-bus-that-was-removed/00-report.md)).
-The program answering is the predecessor's tool server, started on the workstation by hand, with the
-predecessor's broker address written into the assistant's own configuration. It has no manifest, no
-assignment, no account on the bus, and the mesh has never known it exists. Nothing regressed: the
-mesh removed a transport that a program outside the mesh still dials.
-
-**The mesh has a tool model, and it works.** A module serves each tool on its own subject and its
-account may serve nothing else ([ADR 0047](0047-a-module-runs-its-code-as-its-own-process-with-its-own-account.md));
-a person is issued an account whose only permission is to publish the tool subjects named at issue
-([25 — The bus on NATS](../03-DESIGN/01-to-be/25-the-bus-on-nats.md) §7); a client on the
-runtime repository's main branch speaks that account as a command line and as an MCP server. Measured
-on the live mesh on 2026-09-28: a call to the forge's `gitea_list_repos` answered with real
-repositories; the tool list came back empty, because it asks the catalogue for a tool nothing serves
-([ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md)).
-
-**Two records have already said where the surface belongs.** ADR 0132's consequences: *the MCP
-surface belongs inside the mesh — a module the mesh assigns to the machine where the agent sits, with
-a credential the mesh minted and authority derived from what it may call, not a program started by
-hand with a credential printed to a terminal.* Design [33](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md)
-§6 says the same. Neither is a decision about the surface: 0132 decided where a seat's tools live,
-and named the surface in passing.
-
-**And one record says the opposite, in the letter.** [ADR 0095](0095-the-control-plane-is-the-way-to-ask-a-module.md)
-made the control plane *the* way to ask a module, deferred "calling is a grant" until something asked
-for it, and recorded that *nothing outside the control plane can*. A person's account (design 25 §7,
-built 2026-09-28) is exactly that grant, minted for a person. So 0095's exclusivity has already been
-widened once without a record saying so; a module that calls tools widens it a second time, and this
-record is where that is said.
-
-**What a module's account may do today, counted from the composition** (`internal/broker`): publish
-its own events, publish the accept subjects of seats it uses, subscribe its own tools and what it
-consumes. No module principal may publish another module's tool subject. Of 72 modules in the
-catalogue, 45 serve tools and 0 may call one.
-
-The question the work order asks before any code: **does the mesh grow its own operator surface, or is
-the surface an ordinary module that happens to serve tools?**
-
-## Considered Options
-
-**1. The control plane serves the agent protocol itself** — a listener on the controller, or a verb
-its binary runs. Rejected. It puts a surface for tools the control plane does not implement on the one
-component that must stay answerable while it is itself being replaced, which is the reason 0132
-rejected the control plane as the answer to discovery. A person on a workstation would reach it over
-the network, and the networked surface [ADR 0035](0035-one-implementation-several-surfaces.md)
-reserves for that authenticates through an OAuth2 provider that is not configured — so the controller's
-`api` verb correctly serves nothing today, and this option would either wait for it or bypass it.
-
-**2. A program a person installs and starts by hand with a printed credential** — what exists on the
-runtime repository's main. Rejected as the end state. It is outside the mesh in every way issue 147
-names: no assignment, no declaration, no seat, no check that it reaches anything, revoked only by a
-person remembering to. It is the predecessor's arrangement one bus later, and it fails the same way
-the next time an address moves. It stays as the recovery path, the way the command line does
-(ADR 0035): a credential from `operator issue` and the `mesh` client work with no console assigned.
-
-**3. An ordinary module, assigned to the machine the person sits at, holding a credential the mesh
-minted, serving the mesh's tools on that machine's loopback.** Chosen.
-
-## Decision
-
-**The console is a module.** `mesh-console` is built by the mesh, registered like any module,
-assigned to a machine, and given a bus credential sealed to that machine. It serves the mesh's tools to
-whoever is on that machine: to an agent over MCP, and to a person through the same endpoint. Assigning
-it to a machine is what makes the mesh reachable from there; unassigning it revokes that, at the next
-composition, with nothing on the machine to remember to remove.
-
-**A grant to call is a manifest word: `invokes`.** A module declares the tools it calls, each as
-`.`, or the single entry `*` for every tool on the mesh. The bus grants exactly that
-publish side and nothing beside it — no event, no subscription, no seat. This is ADR 0095's deferred
-first option, taken now that a consumer asks for it; a person's account already has this shape, and
-the same composition derives both. The control plane's `ask` stands, and 0095's audit point with it:
-every call still passes one account whose permission list says what it may ask.
-
-**Authority is the machine's login.** The console listens on the machine's loopback only, declared
-`from: machine`, so whoever can open a socket on the machine is whoever owns the machine, and *the
-account that installed the host owns the mesh on that node*
-([ADR 0034](0034-the-local-account-owns-the-mesh.md)). Anything on a machine may call anything on it,
-and that is the whole of local ([ADR 0144](0144-anything-on-a-machine-may-call-anything-on-it.md)).
-The mesh knows no person: what the audit sees is which console asked, under the account
-`.mesh-console`. How a person's identity reaches a session is the question design 15 leaves
-open, and this record does not close it.
-
-**What the console lists is asked of the modules.** Design 33 §5: a module's own tools are answered by
-the module, from the code that defines them. The tool runtime therefore answers one reserved verb for
-every module it serves — `tools`, the module's tool names, descriptions and argument schemas — and the
-console assembles its list by asking the catalogue which modules the mesh holds and each module what
-it answers. A module that is not running is absent from the list and says so; a tool an agent already
-knows the name of can be called whether or not it was listed. A module may not name a tool of its own
-`tools`; the runtime refuses the collision at load rather than letting one shadow the other. A
-role's tools, and the mesh's own verbs, join the list when the `mesh-controller` seat serves them
-(design 33 §1, third family) — the console reads whatever the mesh can say about itself, and grows as
-that does.
-
-**The console holds one credential and one grant: `*`.** It is the operator's surface on a machine the
-operator owns; narrowing what it may call is a setting on its assignment, which
-[ADR 0046](0046-a-module-configuration-is-its-assignments-not-its-manifest.md) already provides for
-and nothing here builds.
-
-> **The mechanism changed — 2026-10-02, by [ADR 0175](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md).** The surface stays a module assigned per node, on loopback, with the machine's login as the authority. It is no longer a container: it is the node tools runtime's serving mode, host-side, and that runtime also serves every assigned module's tools. The module is renamed `node-tools`.
-
-## Consequences
-
-- **The way a person drives the mesh is inside the mesh.** It is declared, delivered, replaced and
- revoked by the same machinery as everything else, and `status` says whether the machine carrying it
- has applied. Issue 147's shape — a surface kept alive by an address in a file — cannot recur, because
- there is no file: the console's credential names the bus the mesh is on, and moves when it does.
-- **A module may now call tools, which ADR 0095 had reserved to the control plane.** The grant is
- explicit, per tool or `*`, and derived by the same composition that grants everything else. A module
- that declares no `invokes` gains nothing. What got harder: a manifest reviewer has one more field to
- read for authority, and `*` in it deserves the reader's attention every time.
-- **A new manifest word ships one release before any manifest uses it**, and must reach both parsers:
- the build machine's and the running controller's. The console's manifest cannot be registered until
- the controller and the builder that packages it have been rebuilt with the word.
-- **Discovery costs a fan-out per list.** One request per module the mesh holds, answered at once by
- the bus for every module nothing serves, so the cost is bounded by the modules that are up. The list
- is cached briefly in the console; a module assigned a moment ago appears at the next refresh.
-- **Every tool runtime must be rebuilt once** to answer `tools`. Until a module is, it is callable and
- not listed, and the console says which modules did not answer.
-- **The mesh's own verbs are not in the console yet.** `status`, `push`, `assign` are the
- `mesh-controller` seat's tools under 0132, and the three prerequisites 0132 names are still not in
- place. A person asking what a node runs still opens a shell for that question, and that gap is design
- 33's to close, not this record's — recorded here so nobody reads the console as the whole of 147.
-- **The person's credential is not retired.** `operator issue` and the `mesh` client remain the path
- when no console is assigned, and the path an operator uses to bring a mesh up far enough to assign
- one.
-
-## How this is checked
-
-| Rule | Checked by |
-|---|---|
-| A module's `invokes` becomes exactly that publish grant, and nothing else | the bus user composition test: a module invoking `shop.price` may publish that subject and no other tool's; `*` may publish every tool subject; neither may publish an event or subscribe anything it did not consume |
-| A malformed `invokes` entry is refused at registration | a parser test: an entry naming no tool is a problem named in the manifest's words |
-| The runtime answers `tools` for every module it serves | the runtime's test: a module registering two tools answers three names, and a module naming one of its own `tools` is refused at load |
-| The console's list is what the modules answer | the client's test against a real bus: two modules up, a third the catalogue holds and nothing serves, and the list carries the two and names the third as not answering |
-| A call from the console reaches a module over the bus | the same test, and the live mesh: the console assigned to a workstation answers `tools/list` on its loopback and a call to the forge returns repositories |
-| The console listens on loopback and nowhere else | its manifest declares `from: machine`, and the filter composed for the machine opens nothing for it |
-
-## References
-
-- [issue 147](../04-ISSUES/147-the-operators-tools-still-dial-the-bus-that-was-removed/00-report.md) — the surface outside the mesh
-- [ADR 0095](0095-the-control-plane-is-the-way-to-ask-a-module.md) — extended: a grant to call, for a module as for a person
-- [ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md) — where a seat's tools live, and the sentence that named the surface
-- [ADR 0035](0035-one-implementation-several-surfaces.md) — three surfaces over one implementation
-- [ADR 0034](0034-the-local-account-owns-the-mesh.md), [ADR 0144](0144-anything-on-a-machine-may-call-anything-on-it.md) — why loopback is the authority boundary
-- [33 — The tools the mesh answers](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md) §5, §6 — discovery, and what serves it to an agent
-- [34 — The console](../03-DESIGN/01-to-be/34-the-console.md) — the design this record authorises
-- mesh-tools `src/client.ts`, `src/mcp.ts`, `src/mesh.ts` — the client this makes a module of
diff --git a/02-DECISIONS/0153-the-record-is-read-by-a-module-and-the-console-lists-it.md b/02-DECISIONS/0153-the-record-is-read-by-a-module-and-the-console-lists-it.md
deleted file mode 100644
index d105a79c..00000000
--- a/02-DECISIONS/0153-the-record-is-read-by-a-module-and-the-console-lists-it.md
+++ /dev/null
@@ -1,113 +0,0 @@
----
-topic: how we work
-status: accepted
-date: 2026-09-30
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0025-the-design-record-is-read-not-copied.md
----
-
-# 153. The record is read by a module the mesh assigns, and the console lists it
-
-## Context
-
-[ADR 0025](0025-the-design-record-is-read-not-copied.md) decided that this repository is **read where
-it is written, never copied to be found**: an agent reads it directly, and a search of the mesh's
-memory consults that agent so its answers appear beside ordinary results. It named the check that
-closes [issue 006](../04-ISSUES/006-hq-is-not-indexed-into-the-knowledge-base/00-report.md): search
-for a phrase that appears only in a design document here, and get it back. It gated the build on an
-agent that did not exist — the mesh session of
-[15 — The agent session](../03-DESIGN/01-to-be/15-the-agent-session.md) — and on a search that
-does not exist either, now: the knowledge base 0025 meant was the predecessor's, and since the
-cut-over nothing reaches it ([issue 147](../04-ISSUES/147-the-operators-tools-still-dial-the-bus-that-was-removed/00-report.md)).
-
-**So the two halves of 0025 have no home.** There is no store to be "beside", and no session to be
-the reader. What the mesh has instead, since today: a tool model in which every module answers what
-it serves, and a console on the machine a person sits at that lists every tool the running modules
-answer ([ADR 0152](0152-the-operators-surface-is-a-module-the-console.md)). An agent holding the
-console does not search a store; it reads a tool list and calls what fits the question.
-
-**What 0025 could not tolerate was a derived copy** — the enforced copy winning while the reasoned one
-quietly stops being true. It rejected a sync for that reason and for no other. A git checkout is not a
-derived copy: it is the same bytes at a commit the answer names, and the only way it can differ from
-the source is by lagging behind it, which is measurable and stated. 0025's own words allow it —
-*retrieval is an agent reading this repository, not a copy living in a second store* — and the
-transformation that makes a copy dangerous is exactly what a checkout does not do.
-
-## Considered Options
-
-**1. Wait for the mesh session.** Rejected. Design 15 is `designed` with nothing built, its model
-access is a provisions question with no consumer identity yet, and 006 has waited since 2026-08-23.
-A record whose check cannot run is a rule enforced by nothing.
-
-**2. The console reads the repository itself.** Rejected. The console holds nothing and decides
-nothing (ADR 0152, ADR 0035); a reader inside it would be a second implementation of a thing that
-should be one module, unavailable to a person's client and to any other module.
-
-**3. A module that keeps a checkout of the repository and answers questions about it, listed by the
-console like any tool.** Chosen.
-
-## Decision
-
-**The reader is a module: `records`.** It requires the `git` provision — the forge — clones the
-repository its settings name, keeps the checkout current on every merge the forge announces and on a
-timer, and answers over the bus: where a phrase appears as written (document, line, nearest heading),
-one document whole, what a folder holds, and where the checkout stands — always with the commit it
-read. Nothing is indexed, ranked or summarised: a design record is found by its own words, and a
-reader deciding which words matter would be a second opinion about somebody else's document.
-
-**The repository is a setting, not a manifest field.** The module names no mesh
-([ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md)): which repository it reads is
-the assignment's business, and an installation that keeps its record elsewhere sets that. Until a
-repository is set it serves no tools and says why. Public repositories only; it asks for no
-credential, because a secret it did not need would be one more thing to seal.
-
-**"Beside everything else" is the console's tool list.** 0025's second half — the search consults the
-agent — has no store to consult and needs none: the console lists `records_search` beside the forge's
-tools and the mesh's own verbs, with a description that says when to call it, and an agent choosing
-tools for a symptom is the search. That is surfacing, not merely reaching: nobody has to know this
-repository exists to be offered it.
-
-**Reading stays one-way.** The module reads the forge and answers; nothing flows back into the
-repository. It holds no credential that could write.
-
-**The mesh session, when it exists, is a caller of this module, not a replacement for it.** Design 15's
-*it holds the design record by reading it* is satisfied by asking `records`; the session brings
-judgement, this brings the text.
-
-## Consequences
-
-- **Issue 006 closes on 0025's own check**, run through the console: `records_search` for a phrase
- that appears in one design document here returns that document. The module's test does the same
- against a repository it makes.
-- **The as-is knowledge document is rewritten.** [`07-knowledge.md`](../03-DESIGN/00-as-is/07-knowledge.md)
- described the predecessor's two stores; neither is reachable from the mesh, and what the mesh knows
- is now what its modules answer. Saying otherwise is the failure this repository exists to name.
-- **A checkout lags.** Between a merge and the next sync — seconds when the forge announces it,
- minutes when it does not — an answer is the previous commit's, and says which. That is the cost of
- no copy, and it is a number rather than a silence.
-- **The reader depends on the forge module's event, by name.** `consumes: gitea.pull.merged` names a
- module rather than the `git` seat, because the seat declares no events. A forge that is not gitea
- leaves the timer as the only refresh, which still works.
-- **What got harder:** the record is now reachable from every machine holding a console, which is what
- was wanted, and a reader must remember that this repository is public and the mesh is not — the
- module reads the public repository and nothing about the installation.
-
-## How this is checked
-
-| Rule | Checked by |
-|---|---|
-| A phrase in one document comes back from where it is written, with the commit | the module's test against a repository it makes; and live, through the console |
-| A merge on the origin is pulled and the next answer names the new commit | the same test |
-| A path outside the checkout is refused, not resolved | a test per shape |
-| A failed sync leaves the checkout standing and is said | a test against an unreachable origin |
-| Without a repository set, no tools are served and the log says why | the module's own start |
-| The console lists `records_search` beside every other tool | the console's listing, live |
-
-## References
-
-- [ADR 0025](0025-the-design-record-is-read-not-copied.md) — extended: the reader is a module, the search is the console's list
-- [ADR 0152](0152-the-operators-surface-is-a-module-the-console.md) — what lists it
-- [issue 006](../04-ISSUES/006-hq-is-not-indexed-into-the-knowledge-base/00-report.md) — what closes
-- [35 — Reading the record](../03-DESIGN/01-to-be/35-reading-the-record.md) — the design
-- mesh-catalog `modules/records` — the module (PR 183)
diff --git a/02-DECISIONS/0154-the-meshs-own-verbs-are-the-controller-seats-tools.md b/02-DECISIONS/0154-the-meshs-own-verbs-are-the-controller-seats-tools.md
deleted file mode 100644
index 62602512..00000000
--- a/02-DECISIONS/0154-the-meshs-own-verbs-are-the-controller-seats-tools.md
+++ /dev/null
@@ -1,140 +0,0 @@
----
-topic: the mesh
-status: accepted
-date: 2026-09-30
-deciders: jochen
-reconstructed: false
-extends: 02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md
----
-
-# 154. The mesh's own verbs are the mesh-controller seat's tools, and which verbs those are
-
-## Context
-
-[ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md) decided that a seat's protocol
-carries its tools in full, that holding a seat means serving them, and that the mesh's own verbs are
-the `mesh-controller` seat's. It named three prerequisites, none in place: the protocol in the store
-rather than in compiled defaults; a protocol richer than a list of verbs; a node-scoped seat's subject
-carrying the node. And it left one thing to a decision per seat: **which verbs each seat serves**,
-because a seat's tools bind every future holder.
-
-The console shipped the same day ([ADR 0152](0152-the-operators-surface-is-a-module-the-console.md))
-and made the gap visible from the operator's chair: a person on a workstation could call every tool a
-*module* serves and none of the mesh's own. What a node runs, what is assigned, whether a push
-applied — the questions issue 147 opened with — still meant a shell on the control node. The console's
-own handshake said so.
-
-The control plane already answers every one of those questions, as commands: `status --json`,
-`node show`, `plan --json`, `assign`, `push`. [ADR 0035](0035-one-implementation-several-surfaces.md)
-says a surface is an adapter over those with no decisions in it, and the `api` verb proves the shape:
-every route calls the function the command line calls.
-
-## Considered Options
-
-**1. Leave the mesh's verbs to the shell until an identity provider authenticates the HTTP API.**
-Rejected. The authenticated network surface is for a browser on another machine; the console is
-already behind the machine's login (0152), and the bus already carries every other tool call under an
-account whose permission list says what it may ask. Waiting would keep the one surface the mesh has
-from answering the mesh's own questions, for a reason that does not apply to it.
-
-**2. Serve the verbs as the mesh-controller *module's* tools, `mesh.mod.mesh-controller.tool.