45 Commits
Author SHA1 Message Date
mesh-tools 34e3321c7f Merge pull request 'Name ADR 0465, 0466 and 0469 in design 47, and write the day's lessons on watchers, pushes and what a merge moves into playbook 07' (#445) from design/47-adr-0465-0466-0469 into main 2026-10-20 19:48:09 +00:00
mesh-tools 2713e131b4 Name ADR 0465, 0466 and 0469 in design 47, and write the day's lessons on watchers, pushes and what a merge moves into playbook 07
mesh/merge-gate head 2713e131b437 merged into trunk def2c09308cd — pass: the change touches no module of the mesh's graph
mesh/repo-check head 2713e131b437 merged into trunk def2c09308cd — pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-20 19:44:57 +00:00
mesh-tools def2c09308 Merge pull request 'Name ADR 0457 and ADR 0463 in design 47, and write the day's delivery lessons into playbook 07' (#444) from design/47-adr-0457-0463-and-lessons into main 2026-10-20 18:03:06 +00:00
mesh-tools 5705da30c0 Judge a catalogue removal by the controller's unit tests against the catalogue's branch, not by a try
mesh/merge-gate head 5705da30c006 merged into trunk a5c6c98ecca0 — pass: the change touches no module of the mesh's graph
mesh/repo-check head 5705da30c006 merged into trunk a5c6c98ecca0 — pass: its merge-check.sh passed
mesh/delivery delivered
A controller try reads the catalogue's main, so it would have passed for mesh-catalog #381 (review round 1).
2026-10-20 17:59:47 +00:00
mesh-tools 1d7cfed47a Name ADR 0457 and ADR 0463 in design 47, and write the day's delivery lessons into playbook 07
Design 47's catch-up of merges nobody heard is decided by ADR 0457 and bounded by ADR 0463, so other sessions read them from its frontmatter. Playbook 07 gains four lessons from 2026-10-20: a Retry is never asked beside an Approve (issue 1408), an ask is named only after it is read on the router, a check's wait is read from its delivery and not the build queue, and a catalogue change removing a module is tried against the controller (issue 1406).
2026-10-20 17:57:57 +00:00
mesh-tools a5c6c98ecc Merge pull request 'Design 59: milestones 2 to 4 confirmed with their issues, and the mail modules' as-is design' (#443) from design-59-milestone-issues into main 2026-10-20 17:45:08 +00:00
jochen d2dccddc27 Say the notifier's bundle is its own process under its own account, as main.go says
mesh/merge-gate head d2dccddc2726 merged into trunk 0738fa1405dd — pass: the change touches no module of the mesh's graph
mesh/repo-check head d2dccddc2726 merged into trunk 0738fa1405dd — pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-20 19:12:04 +02:00
jochen dcc6f185d4 Answer the review: the mail modules are separate modules, the notifier has no process, nothing future in the as-is; 1433 depends on none 2026-10-20 19:11:06 +02:00
jochen e925a843ce Write the mail modules' as-is design, so what design 59's milestone 2 replaces is recorded before it is built
Playbook 04 step 3: the one grants list that gives reading and sending at once, and mesh-mail-notifier's pop-up for every new mail, read from mesh-catalog main a67f0196 and the live mesh.
2026-10-20 19:08:13 +02:00
jochen 66bb013b85 Write milestones 2 to 4's issues into design 59, as the operator confirmed them
Playbook 04 step 6. Also names ADR 0378 §5 beside ADR 0440 §4 for the page's use of mail-reading, its writes with correct and undo, and issue 1419 in §8's last sentence, as the graduation's review noted.
2026-10-20 19:02:56 +02:00
mesh-tools 0738fa1405 Merge pull request 'Graduate research 066 into design 59: mail reading, mail signals, reading and sending granted apart, the mail conversation; Mail as the thirteenth domain' (#442) from research-066-graduate into main 2026-10-20 14:34:18 +00:00
jochen a0a4edabc9 Space mail reading's batches 160 s apart, as ADR 0459 §15 now says
mesh/merge-gate head a0a4edabc9df merged into trunk ae9768c2c115 — pass: the change touches no module of the mesh's graph
mesh/repo-check head a0a4edabc9df merged into trunk ae9768c2c115 — pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-20 16:25:02 +02:00
jochen ebc7665b2a Say that milestone 4 waits for the decision on its thread's caller 2026-10-20 16:22:53 +02:00
jochen f96f964a2d Answer the review: carry test 4 over as the direction words it, make milestone 4 wait for milestone 2, fix ADR 0461's clause numbers in the glossary 2026-10-20 16:21:49 +02:00
jochen 4a2c15f725 Graduate research 066 into design 59: mail reading, mail signals, granted reading apart and the mail conversation, with the Mail domain in the glossary
Research 066's confirmed direction becomes ADR 0458 to 0462. Design 59 gains the sections and milestones 2 to 4 they decide, the glossary gains Mail as the thirteenth domain with four homonym rows, and design 49 lists Config and Mail so its count matches the glossary.
2026-10-20 16:17:50 +02:00
mesh-tools ae9768c2c1 Merge pull request 'Cut design 47's release 4 into R4a to R4g, and date each 'release 4' rule by the R4 release that builds it (ADR 0453)' (#441) from design/47-release-4-cut into main 2026-10-20 06:14:18 +00:00
mesh-tools 440279fec4 Date the SDK-before-users order of design 47's release 1-2 note by R4e
mesh/merge-gate head 440279fec472 merged into trunk f09cc22d9915 — pass: the change touches no module of the mesh's graph
mesh/repo-check head 440279fec472 merged into trunk f09cc22d9915 — pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-20 06:07:29 +00:00
mesh-tools 3439d19d06 Answer review round 1 of design 47's release 4 cut: dates of the status, the builder, test 4 and the issues 2026-10-20 06:07:17 +00:00
mesh-tools 89c4295f6d Name release 4's seven releases R4a to R4g in design 47's table (ADR 0453) 2026-10-20 06:00:37 +00:00
mesh-tools 3e061bbd99 Cut design 47's release 4 into R4a to R4g by the orders it hides (ADR 0453) 2026-10-20 06:00:11 +00:00
mesh-tools f09cc22d99 Merge pull request 'Cut design 47's R3d into R3d and R3e, and name the terminal-free fix in R3d (ADR 0450, ADR 0449)' (#440) from design/47-r3d-and-r3e into main 2026-10-19 21:45:15 +00:00
mesh-tools 6ffd3709d8 Cut design 47's R3d into R3d and R3e, and name the terminal-free fix in R3d (ADR 0450, ADR 0449)
mesh/merge-gate head 6ffd3709d8bf merged into trunk 990a77c7605e — pass: the change touches no module of the mesh's graph
mesh/repo-check head 6ffd3709d8bf merged into trunk 990a77c7605e — pass: its merge-check.sh passed
mesh/delivery delivered
R3d's prepare step found four things the gate reads that must ship before it, and two steps that needed the operator's terminal; R4 gains the controller's deletions (1318).
2026-10-19 21:27:20 +00:00
mesh-tools 990a77c760 Merge pull request 'Answer design 47's review for release 4's check (follow-up to #438)' (#439) from design/47-the-check-in-r4-review into main 2026-10-19 20:50:51 +00:00
jschoubben cc605e3b13 Answer design 47's review for release 4's check: the judge's account ends at release 4, the assigner's reads and version skew and field refusal stated, where acceptance tests 22 and 23 are measured, and what release 4 retires
mesh/delivery delivered
mesh/merge-gate out of date: judged against trunk 023ad0116034, the trunk is now 990a77c7605e; judged again on a recheck or when it is merged
mesh/repo-check out of date: judged against trunk 023ad0116034, the trunk is now 990a77c7605e; judged again on a recheck or when it is merged
2026-10-19 22:44:18 +02:00
mesh-tools 023ad01160 Merge pull request 'Design 47's release 4 gains the check: only what changed runs, tests by the language's build module, the composition by the assigner, one status (ADR 0444 to ADR 0447)' (#438) from design/47-the-check-in-r4 into main 2026-10-19 20:12:07 +00:00
jschoubben 9ffa60924e Graduate research 058 into design 47's release 4: a check runs only what changed, its tests by the language's build module, its composition by the assigner, in one status
mesh/merge-gate head 9ffa60924ee9 merged into trunk c6d36b2f32c3 — pass: the change touches no module of the mesh's graph
mesh/repo-check head 9ffa60924ee9 merged into trunk c6d36b2f32c3 — pass: its merge-check.sh passed
mesh/delivery delivered
The check took 3 to 4.5 minutes on every change and paid the whole suite again on every re-ask; the operator's target leaves it about a minute. ADRs 0444 to 0447 decide how; the glossary gains composition check and kept verdict, and merge gate is struck once release 4 is delivered (issue 1303).
2026-10-19 22:03:44 +02:00
mesh-tools c6d36b2f32 Merge pull request 'Design the mail web client: mesh-mail-web on the private network behind Keycloak (design 59, ADR 0440)' (#437) from design/59-the-mail-web-client into main 2026-10-19 18:44:29 +00:00
jochen f6311a66bf Answer design 59's review: mesh-mail's caller check, tests for the send state and the seat's verbs, no tracker state in the design; milestone 1 starts
mesh/merge-gate head f6311a66bf66 merged into trunk bea864af8bcd — pass: the change touches no module of the mesh's graph
mesh/repo-check head f6311a66bf66 merged into trunk bea864af8bcd — pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-19 20:36:39 +02:00
jochen c04c0ebb76 Name design 59's milestone issues, confirmed by the operator 2026-10-19 16:41:32 +02:00
jochen 491de157da Design the mail web client: mesh-mail-web on the private network behind Keycloak, reading and sending through mesh-mail (ADR 0440); cite ADR 0439 in the as-is of a module's state 2026-10-19 16:38:55 +02:00
mesh-tools bea864af8b Merge pull request 'Keep three lessons of feature mail-sender in playbook 07: a rule keyed on a shared property, a manifest word's first push, one watcher per wait' (#436) from docs/playbook-07-lessons-mail-sender into main 2026-10-19 13:30:09 +00:00
jochen 7ed83f4965 Keep three lessons of feature mail-sender in playbook 07: a rule keyed on a shared property, a manifest word's first push, one watcher per wait
mesh/merge-gate head 7ed83f4965de merged into trunk 71759e83f873 — pass: the change touches no module of the mesh's graph
mesh/repo-check head 7ed83f4965de merged into trunk 71759e83f873 — pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-19 15:28:33 +02:00
mesh-tools 71759e83f8 Merge pull request 'Name R3c's issues and the forge module it takes in, and the orders its prepare step found (design 47)' (#435) from docs/r3c-row into main 2026-10-19 12:48:05 +00:00
jschoubben 3174d5aa6e Name R3c's issues and the forge module it takes in, and the orders its prepare step found (design 47)
mesh/merge-gate head 3174d5aa6ef5 merged into trunk e7c8ce274060 — pass: the change touches no module of the mesh's graph
mesh/repo-check head 3174d5aa6ef5 merged into trunk e7c8ce274060 — pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-19 14:46:33 +02:00
mesh-tools e7c8ce2740 Merge pull request 'Keep the drawn overview of the playbooks in hq, for a later docs site' (#434) from docs/playbooks-page into main 2026-10-19 11:31:12 +00:00
jschoubben 5816ca669f Say the page's freshness is not yet checked (issue 1222), and fix 08's leftover 'reviewed after its push' (review round 1)
mesh/merge-gate head 5816ca669f20 merged into trunk f269e83f5d1a — pass: the change touches no module of the mesh's graph
mesh/repo-check head 5816ca669f20 merged into trunk f269e83f5d1a — pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-19 13:29:22 +02:00
jschoubben afe83b9124 Keep the drawn overview of the playbooks in hq, for a later docs site 2026-10-19 13:27:55 +02:00
mesh-tools f269e83f5d Merge pull request 'Try before the review, fail first on behaviour, survey every reader of a changed value (playbooks 07 and 08)' (#433) from docs/review-speed into main 2026-10-19 11:08:50 +00:00
jschoubben 6c607504de Say a moved trunk is met by the merge check, not promised to step 3 (review round 2)
mesh/merge-gate head 6c607504de1b merged into trunk 7e1c0f4c1564 — pass: the change touches no module of the mesh's graph
mesh/repo-check head 6c607504de1b merged into trunk 7e1c0f4c1564 — pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-19 13:05:09 +02:00
jschoubben 20c857443c Bring the branches up to the trunk before the try, and say the try at step 1's end runs the integration tests (review round 1) 2026-10-19 13:04:30 +02:00
jschoubben 7735cfd107 Try before the review, fail first on behaviour, survey every reader of a changed value (playbooks 07 and 08) 2026-10-19 13:03:28 +02:00
mesh-tools 7e1c0f4c15 Merge pull request 'Design 47: R3b's issues, and the two facts its prepare step found for R3c's insight on ADR 0437' (#432) from docs/design-47-r3b-issues into main 2026-10-19 09:53:21 +00:00
jschoubben 19a8e92801 Design 47: R3b's issues, and the two facts its prepare step found for R3c's insight on ADR 0437 (issues 1210-1218)
mesh/merge-gate head 19a8e9280103 merged into trunk 59c6954b0cf8 — pass: the change touches no module of the mesh's graph
mesh/repo-check head 19a8e9280103 merged into trunk 59c6954b0cf8 — pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-19 11:51:15 +02:00
mesh-tools 59c6954b0c Merge pull request 'Design 47: R3a's issues, the two controller fixes it needed first, and R3c's open question on Go-compiling images' (#431) from docs/design-47-r3a-issues into main 2026-10-19 09:13:16 +00:00
jschoubben 95bf5cbf20 Design 47: R3a's issues, the two controller fixes it needed first, and R3c's open question on Go-compiling images (issues 1200-1204, 1208, 1209)
mesh/merge-gate head 95bf5cbf2089 merged into trunk f0bcc75fcfce — pass: the change touches no module of the mesh's graph
mesh/repo-check head 95bf5cbf2089 merged into trunk f0bcc75fcfce — pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-19 11:09:15 +02:00
15 changed files with 1311 additions and 61 deletions
+1 -1
View File
@@ -15,7 +15,7 @@ works — plus the engineering practice that holds across everything Novox build
## Rules
- Markdown only.
- Markdown only, but for the checks and one drawn overview of the playbooks (`process/playbooks.html`).
- **Stable by nature.** Changes here reflect a genuine shift in intent, not iteration. The one
exception is `how-we-build.md`, which changes whenever a rule is earned — and only through
its amendment process.
+73 -8
View File
@@ -403,14 +403,23 @@ and the operator act. **Recorded by** the controller's `identity` and `licences`
back when it fails. The domain is continuous integration and delivery, named **delivery**, and its parts live in
the repository `mesh-delivery` (ADR 0429 §2). **Recorded by** the `mesh-delivery` module (deliveries) and the
controller (the planner, the rollout) until release 6 of ADR 0429; from each release on, by the parts it builds. **Downstream of** Module, Core, Health and repair and Data.
**Decided by** ADR 0162, 0218, 0236, 0237, 0238, 0239, 0250, 0267, 0270, 0276, 0323, 0401–0408, 0428, 0429.
**Decided by** ADR 0162, 0218, 0236, 0237, 0238, 0239, 0250, 0267, 0270, 0276, 0323, 0401–0408, 0428, 0429, 0444–0447.
**Uses:** module, node, declaration and send (Placement), health (Health and repair), person.
- **merge check** — the two statuses a pull request carries before it may merge: the **merge gate**
(`mesh/merge-gate`, the build seat's composed check of the modules the change touches) and the
**repository check** (`mesh/repo-check`, the repository's own `merge-check.sh`)
(ADR 0238). Both judge the merge of the pull request's head into a named **trunk commit**, made in the
build seat's own clone and never pushed (ADR 0323).
- **merge check** — what a pull request carries before it may merge, judging the merge of its head into a
named **trunk commit**, made in the builder's own clone and never pushed (ADR 0323). Until release 4 of
ADR 0429 is delivered, two statuses: 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). From that release, one status, `mesh/check`, written by the checker: the
change's tests, run by the language's build module, and the **composition check**; for a delivery group, one
verdict on every member's head (ADR 0445, ADR 0447).
- **composition check** — whether the machines a change touches still compose with it, run by the assigner
from the modules the indexer and the builder hold, with the composition the controller sends with
(ADR 0446). It replaces the merge gate from release 4 of ADR 0429; the old word is struck once that release
is delivered and every design that still says it is reworded (issue 1303).
- **kept verdict** — a package's passing verdict at one test level, kept by the checker under the digest of
everything its tests read, written once and read-only; a check takes it instead of running an unchanged
package (ADR 0444).
- **trunk commit** — the commit of the trunk a merge check's verdict was judged against: the trunk's head when
the build seat cloned it. The verdict holds for that commit only, and goes out of date when the trunk moves
(ADR 0323).
@@ -763,6 +772,54 @@ it was told, and the servarr apps' own libraries. **Upstream of** nothing. **Dec
- **grab** — an engine handing one media release to a download client. It names the work it is for, so
the media-importer knows where the copy belongs.
## Mail — the operator's own correspondence, which the mesh holds and reads for them
**Purpose.** To hold the operator's mail accounts, say each mail on the bus, find and read mail for whoever the
operator granted, note each mail for the operator and tell them of the mail that matters, and send a mail only on
the operator's Approve. **Recorded by** `mesh-mail` (its index database and attachment bucket, ADR 0359 §3; its
account buckets, ADR 0439) and `mesh-mail-reader` (its database of mail entries, mail groups and reading lessons,
ADR 0459). **Downstream of** Identity and access and Operator and conversation; **upstream of** nothing.
**Decided by** ADR 0359, 0361, 0378, 0439, 0440, 0458, 0459, 0460, 0461, 0462.
**Uses:** operator, person, module, seat; grant, identity (Identity and access); ask, warrant, operator channel,
content rule, untrusted input (Operator and conversation). A *judgement* is one call of the model with no tools
and no session (ADR 0278), in its plain sense.
- **mail account** — one of the operator's mailboxes that `mesh-mail` holds, of a kind of adapter (`imap`,
`imap-oauth`, `graph`), named by its address (ADR 0359 §1).
- **`mail-store`** — the seat holding the mail accounts: `accounts`, `search`, `read`, `send`, `send_status` (ADR
0378 §2, ADR 0440 §7). Always the seat's name: the bare *store* is Core's.
- **mail event** — what `mesh-mail` publishes on its stream `MAIL` for a new, changed or removed mail: headers,
flags and attachment references, never a body (ADR 0359 §2).
- **mail send** — a mail the mesh sends, which goes out only on the operator's warrant for that one mail (ADR 0361
§2), with the states `mail_send_status` answers (ADR 0440 §7). Only a caller granted asking to send for that
account may ask for one (ADR 0460).
- **mail web client** — the module `mesh-mail-web`, holder of `mail-webclient`: the operator's mail in a browser on
the private network (ADR 0440 §1). The page it serves is the mail page.
- **mail notice** — `mesh-mail-notifier`'s desktop pop-up for a new inbox mail (ADR 0378 §7, §9). It retires when
`mesh-mail-notifier` is unassigned everywhere (ADR 0461 §7) and then moves to *mail signal*'s *Not:* line
(issue 1418).
- **mail reading** — the model's reading of each new mail as it arrives, in batches, into its mail group and mail
entry; the seat `mail-reading`, held by `mesh-mail-reader` (ADR 0459). Not *triage* (the issue tracker's
judgement), and not `mail_read`, `mail-store`'s tool that fetches one mail's text.
- **mail entry** — what mail reading keeps about one mail: its mail group, a one-line summary, its mark and why, a
due date; never the body (ADR 0459 §4). Not *catalogue entry*: the catalogue is Delivery's.
- **mark** — what a mail entry says the mail is to the operator: *matters*, *scam warning* or neither, never two
(ADR 0459 §7). Not *kind* (Media's).
- **mail group** — a set of mails the model put together because they belong to one subject (ADR 0459 §4). Not *delivery
group* or *account group*.
- **reading lesson** — one correction the operator made on the mail page (a mark or a mail group), kept so later
mail reading follows it (ADR 0459 §8 to §10). Always qualified: the bare *lesson* is Operator and conversation's.
- **mail signal** — the notice on the operator's channel, one `tell` per batch in words only, and the place at the
top of the mail page, that tells the operator mail matters (ADR 0461). Always qualified: the bare *signal* is
Health and repair's.
- **scam warning** — the mark of a mail the model judges an attempt to deceive the operator, with why; shown in
its own place on the mail page and never a mail signal (ADR 0459 §7, ADR 0461 §6).
- **mail conversation** — a thread (ADR 0383) with a mail context, followed on the mail page: the mail questions
and their answers, each answer with the mails it found. The mail context is the framing that a mail is data, the
tools `mail_search` and `mail_read` only, the operator's senders and folders, and the reading lessons (ADR 0462).
- **mail question** — what the operator writes in the mail conversation to find or understand mail. Not *ask*
(Operator and conversation's request for the operator's input).
## The record — how this repository works
Not a domain of the mesh but of the way it is built, listed because its words meet the mesh's.
@@ -852,7 +909,7 @@ renaming one sense, the old sense moves to a *Not:* line and becomes mechanical.
| | taking a consumer off a licence | **unbind** (verb `release`, to rename) | Identity and access |
| | a copy of a work an engine can fetch | **media release** | Media |
| gate | the judgement on a delivery's first node | **first-node gate** | Delivery |
| | the pull request status | **merge gate** | Delivery |
| | the pull request status, until release 4 of ADR 0429 | **merge gate**; from release 4, **composition check** | Delivery |
| | the part that merges a group and moves the source revision | **gate part** (`mesh-delivery-gate`) | Delivery |
| | a failed step stopping its module's later steps (ADR 0136) | *a failed step holds its module* | Placement |
| tier | a level of the mesh | **layer** | Core |
@@ -868,6 +925,7 @@ renaming one sense, the old sense moves to a *Not:* line and becomes mechanical.
| store | the one database server | **store** | Core |
| | the OCI registry | **artifact store** | Delivery |
| | a node's own copy of its last declaration | **last declaration** | Placement |
| | the seat holding the mail accounts | **`mail-store`** | Mail |
| record | a numbered decision | **decision record** | The record |
| | the literal reader of this repository, searched first for a symptom | **the record** (the `records` module) | Operator and conversation |
| | the upgrade policy that sends nowhere | `record` | Module |
@@ -875,7 +933,8 @@ renaming one sense, the old sense moves to a *Not:* line and becomes mechanical.
| | the operator's terminal command, `mesh-cli` | **the `nox` command** | Operator and conversation |
| knowledge base | the module holding the decision records and research efforts whole | **knowledge base** | The record |
| | the predecessor's operational memory, in older playbooks (issue 430) | none: it no longer exists | — |
| check | a pull request's two statuses | **merge check**, **merge gate**, **repository check** | Delivery |
| check | a pull request's two statuses, one from release 4 of ADR 0429 | **merge check**, **merge gate**, **repository check**; from release 4, `mesh/check` | Delivery |
| | whether the machines a change touches still compose, the assigner's | **composition check** | Delivery |
| | a delivery group's verdict | **composed check** | Delivery |
| | the controller's own examination | **self-check** | Health and repair |
| | a file in `00-META/checks/` | **hq check** | The record |
@@ -893,6 +952,12 @@ renaming one sense, the old sense moves to a *Not:* line and becomes mechanical.
| | a file the mesh writes, such as the agent's instruction file or `managed_settings` | the file's own name, or *a file the mesh places* | — |
| trusted | a config value that decides what runs or who can reach what | **trusted** config value | Config |
| | a sender on the operator's own list | **trusted** sender, its message an **operator message** | Operator and conversation |
| lesson | an agent's proposal after the operator corrects its way of working | **lesson** | Operator and conversation |
| | the operator's correction of mail reading | **reading lesson** | Mail |
| signal | something that must keep happening | **signal** | Health and repair |
| | the notice that mail matters | **mail signal** | Mail |
| conversation | the domain *Operator and conversation* | the domain's name | Operator and conversation |
| | the mail questions and answers on the mail page | **mail conversation** | Mail |
## How this page is kept
+4 -1
View File
@@ -48,6 +48,9 @@ the expensive half.
## The playbooks
A drawn overview of them, the cycle, a feature's steps, the review loop and an issue's life, is
[`playbooks.html`](playbooks.html): a picture of these playbooks, never their authority.
| # | Playbook | Trigger |
|---|---|---|
| [01](01-research.md) | Research | An idea from the operator, or one worth investigating before committing to design: setup questions, findings, a direction the operator confirms |
@@ -57,7 +60,7 @@ the expensive half.
| [05](05-constitution-sync.md) | Constitution sync | `how-we-build.md` changed a rule the mesh enforces |
| [06](06-writing-a-module.md) | Writing a module | Something that runs today must run on the mesh |
| [07](07-feature-branches.md) | Features | Work that changes code across issues or repositories: prepared context, local development, one review, one check, one delivery |
| [08](08-small-fixes.md) | Small fixes | One issue in one repository, about an hour of work: one review, one push, one delivery |
| [08](08-small-fixes.md) | Small fixes | One issue in one repository, about an hour of work: one try, one review, one delivery |
| [09](09-cleanup.md) | Cleanup | Merged branches, stale pull requests, finished worktrees and folders: read first, delete only what the operator approves, report the rest |
## The cycle is checked
+82 -22
View File
@@ -30,6 +30,11 @@ A small fix (one repository, one issue, about an hour of work) follows
- **A green pull request nobody marks.** An approved, green pull request that was not marked ready waited
12 h 17 min for someone to merge it (mesh-catalog #300, 2026-10-14). There is no ready mark since ADR 0421:
the operator's Approve is asked as soon as the checks pass.
- **Review rounds spent on what a run or a search would have shown.** On 2026-10-19 milestone R3b of design 47
took two review rounds and one more cycle after a failed try. A test that "failed first" only by not compiling
proved nothing, and dropping the field it was for passed every test. A reader of the changed record that the
survey never listed ended a module's conditions at each sync. The reviewer judged the bus tests by reasoning, and
wrongly, because they ran only after the review.
- **A module proven only after it reached the machines.** The mail modules were delivered with unit tests
only, then failed live three times (issues 859, 865, 868) and took five more hours of fixes.
@@ -42,13 +47,13 @@ opened **once**, after the review. It runs in five steps, and its time is bounde
| Step | What happens | Bound |
|---|---|---|
| 0. Prepare | the lead settles the open questions and writes one context file per repository | 15 minutes |
| 1. Develop | one fixing agent per repository builds every issue locally, the whole unit suite before every commit | the milestone's issues, 30 minutes each |
| 2. Review | one reviewer reads all of it; reports go both ways; at most 3 rounds | 15 minutes |
| 3. Check | push; `mesh-controller.try` runs the integration tests on each pushed branch, then the pull requests are opened and the forge's merge checks run them again; no further review | one try and one check run |
| 1. Develop | one fixing agent per repository builds every issue locally, the whole unit suite before every commit; then each branch is pushed and tried | the milestone's issues, 30 minutes each, and one try |
| 2. Review | one reviewer reads all of it with the tries' results; reports go both ways; at most 3 rounds | 15 minutes |
| 3. Check | push the reviewed heads; a head the review changed is tried again; then the pull requests are opened and the forge's merge checks run the integration tests again; no further review | at most one more try and one check run |
| 4. Deliver | merge; the delivery plan builds, publishes and sends the whole feature | its delivery plan |
**One milestone is one release** (the operator, 2026-10-17). A milestone is developed whole, reviewed once,
pushed once and delivered once. It is never cut into steps for its size or for time: every cut adds a review, a
**One milestone is one release** (the operator, 2026-10-17). A milestone is developed whole, tried before its review, reviewed once,
opened once and delivered once. It is never cut into steps for its size or for time: every cut adds a review, a
merge check, an Approve and a delivery plan of waiting, so the fewest cuts is the fastest. Milestones are cut by
release order when the design is broken up (playbook [04](04-build-handoff.md)); a release-order dependency found
inside a milestone is a fault of that cut, said to the operator, never a silent extra step.
@@ -93,7 +98,10 @@ never into the running one, unless it blocks the running one.
5. **Write the findings into the issues** (the operator, 2026-10-17: the better the content of the issues, the
better work the agents do, and the less review is needed). Before any agent starts, each issue carries, with
`issue-tracker.note`: every file and test that touches the change, found by search and not from memory
(file:line); how the change is checked and delivered end to end (which test proves its done-when, which
(file:line); **every other reader and writer of each value the change adds or changes** (a field, a key, a kind
of record), each with what it does with the new value, and a criterion for each that could treat it wrongly
(2026-10-19: the router's sync with the controller ended every condition the controller did not list, and the
survey, which followed only the new condition's own path, never named it); how the change is checked and delivered end to end (which test proves its done-when, which
modules its delivery plan moves, to which machines); and for a group, its merge order. The context file
points to these notes; an issue reread by another session or a later agent then holds what was found.
When the reading is large, it starts with a read-only survey agent on the smaller model.
@@ -133,13 +141,20 @@ never into the running one, unless it blocks the running one.
the next instruction, so its context is paid for once.
- **Unit tests only, each well under a second** (how-we-build §5, ADR 0391). **The whole unit suite runs before
every commit** (the operator, 2026-10-17), not only the tests of what changed: it takes under a second, and a
change breaks tests in packages it never touched. Each issue's first test fails before its change and passes
after; `gofmt`, `go vet` and the whole unit suite stay green. **The tests are written from the issue's
change breaks tests in packages it never touched. Each issue's first test fails before its change, on an assertion about what the
code does, and passes after. **A test that fails only because it does not compile has not failed first**: where
the test calls what the change adds, the agent takes out the change's effect (the line that does it, not the new
function), sees the test fail, and puts it back (2026-10-19: a check request's new field was proven by a test of
its helper alone, and dropping the field from the request passed every test); `gofmt`, `go vet` and the whole unit suite stay green. **The tests are written from the issue's
acceptance criteria**: one test or more per criterion at the level it names, each test's comment citing
the criterion it proves by its label (`// 1118/AC2`), and no criterion left without its test; an integration criterion is a test
that compiles and waits for the check step. The integration tests (those that start a store, a bus or a
process) wait for the check step, where `mesh-controller.try` runs them first.
- **Nothing is pushed** and no pull request is opened in this step. Never land on a red check: run the
that compiles and waits for the try at this step's end. The integration tests (those that start a store, a bus
or a process) are run first by that try, and again by the forge's merge check.
- **No pull request is opened in this step. When every issue is committed, the lead brings each feature branch up
to the current trunk (fetch by name, then merge), pushes it and tries it** (`mesh-controller.try`, as step 3 item 1 says), so the review reads the integration tests' real
results and judges none by reasoning (2026-10-19: a reviewer reasoned that a warning goes out at once, the
router holds it 30 s, and the try after the review found it). A failed try goes back to the same fixing agent
with its whole log, read as step 3 item 3 says, before the review starts. Never land on a red check: run the
checks, read the result, then land in a separate step.
- **The fixing agent's brief** (the lead gives it with the context file): name your issue in every step and
message ("issue 805: landing in mesh-catalog"); reach the mesh only through the mesh MCP server, never ssh;
@@ -147,7 +162,7 @@ never into the running one, unless it blocks the running one.
is a numbered migration and the consolidated schema; Go only unless the context file says otherwise; never
move another issue; **never run an integration test on the machine** (no `go test -tags integration`, no `merge-check.sh`,
no store or bus started by hand): compile them with `go vet -tags integration ./...` and leave the running to
the check step, where `mesh-controller.try` runs them on the pushed branch. On 2026-10-17 three fixing agents ran them anyway, one for minutes on end, because the
the lead's try at the end of this step, where `mesh-controller.try` runs them on the pushed branch. On 2026-10-17 three fixing agents ran them anyway, one for minutes on end, because the
brief only said they wait for the check (the operator). Report back in under 150 words: commits, tests added and that they failed first, the
check results, anything you stopped on.
- **An agent that finds the code differs from its context file stops and reports** (file:line); the lead
@@ -162,8 +177,9 @@ never into the running one, unless it blocks the running one.
## Step 2 — Review (one reviewer, larger model; at most 3 rounds, about 15 minutes)
1. **Bring every feature branch up to the current trunk** (fetch by name, then merge), so the reviewed tree is
the tree that is pushed.
1. **The reviewed tree is the tree that was tried**: every feature branch was brought up to the current trunk
before its push and try (step 1). A try and a merge check each run on the merge with the trunk as it is then, so a
trunk that moves later is met by the forge's merge check, not by another merge here.
2. **One reviewer reads every repository's local commits** together, against the context files and the
decisions. **It is one agent, kept for the whole release** like the fixing agents: the lead continues it by
its id for every later round, for the extra cycle after a failed check (step 3) and for its review record, so it
@@ -172,13 +188,20 @@ never into the running one, unless it blocks the running one.
It says which round it is in ("round 1 of at most 3").
3. **The report goes to the same fixing agents** that wrote the code, continued by their ids, so they fix
with the context they already hold. Each answers the reviewer: what it changed, per finding, or why not.
4. **Every new integration test is read against the code it drives before the first push.** It has never run: fixing agents only compile it, and its first run is the merge check, so each fault it hides costs a whole check. The reviewer traces each step of each new integration test through the code (fixtures against the manifest rules, what the store keeps and reads back, who holds a seat), and says which steps it could not trace. On 2026-10-17 a new test failed on its first two runs, on a fixture and then on a fault of the change; reading it against the code after the first failure found another fault, which would have failed a run too. The reviewer also checks that each new integration test enters through the same path the live trigger takes, not through a state it writes by hand: on 2026-10-17 milestone 4's test saved its delivery plan directly, so it skipped the cut from a batch that every live plan takes, and it passed while the backup step never ran (issue 1086).
4. **Every new integration test is read against the code it drives and against its try.** Fixing agents only compile it, and its one run before the review is the try (step 1), which shows whether it passed but not whether it proves what it says. The reviewer traces each step of each new integration test through the code (fixtures against the manifest rules, what the store keeps and reads back, who holds a seat), and says which steps it could not trace. On 2026-10-17 a new test failed on its first two runs, on a fixture and then on a fault of the change; reading it against the code after the first failure found another fault, which would have failed a run too. The reviewer also checks that each new integration test enters through the same path the live trigger takes, not through a state it writes by hand: on 2026-10-17 milestone 4's test saved its delivery plan directly, so it skipped the cut from a batch that every live plan takes, and it passed while the backup step never ran (issue 1086).
5. **The reviewer reads the answers and the new commits**, and approves or reports again (step 7 says what a round
may ask). After round 3 the lead stops and brings the operator what is left, with a recommendation.
6. **The reviewer reads every acceptance criterion against its test**: each criterion has a test at its level
that would fail without the change, and no test proves less than its criterion says. A criterion missing
for a case the issue's done-when needs is written onto the issue (step 0, item 5) and is a finding; a test
missing for a criterion is a finding.
**A rule keyed on a property is checked against everything else on the mesh that has it.** When a change
decides by a property the bus or the store gives (a stream's retention, a folder's name, an account's kind),
the reviewer lists what else already has that property, live, and asks what the rule does to it. On
2026-10-19 release 1 of feature mail-sender made a work-queue stream's dead letters drop-only by the bus's
work-queue retention; three review rounds passed it, and only its try showed that every seat's work stream has
that retention too, so dead asks across the mesh would no longer have been delivered again (issue 1178, fixed
before its pull request by a mark only a declared module work queue carries).
7. **A finding outside the issues' done-when and their acceptance criteria is never a change asked, in any round.** More hardening, a
further case, a nicer form: the reviewer opens it with `issue-tracker.open` and names the number in its
report, and the round does not wait for it. What a round asks is only what makes the change wrong against
@@ -186,21 +209,33 @@ never into the running one, unless it blocks the running one.
## Step 3 — Check (no review)
1. **Push every feature branch, try it, then open one pull request per repository, together.**
**The integration tests run once before the pull requests are opened**: `mesh-controller.try` with the
1. **Push the reviewed heads, try again a head the review changed, then open one pull request per repository,
together.** **The integration tests run before the pull requests are opened**, first before the review (step 1)
and again for a head the review changed since its try: `mesh-controller.try` with the
repository and the pushed branch runs the repository's `merge-check.sh` (main's version) on the merge, in the
toolchain containers and against the throwaway store and bus a merge check gets, and answers with an id to
read back. A try that fails goes to step 3 item 3 as a failed check does; the pull requests are opened only
when every try passed (the operator, 2026-10-18: three pushes in a row failed their first merge check on
when every head's last try passed (the operator, 2026-10-18: three pushes in a row failed their first merge check on
faults a try finds in minutes). Never `merge-check.sh`, `go test -tags integration` or a store or bus started
by hand on the machine (the operator, 2026-10-18). Pull requests in two or
more repositories that share the branch name are one delivery group (ADR 0239 §2).
Where a session pushes through the forge tool `gitea_commit` (one commit on the forge's current base), the
reviewer first confirms that commit's tree equals the reviewed one. **Each pull request's description carries
reviewer first confirms that commit's tree equals the reviewed one. **A large change is pushed by a pusher agent** on the smaller
model: `gitea_commit` patches of about 30 KB each, a file copied from another path sent as a copy (`copy_from`)
and not as its bytes, and the forge's tree compared with the local one after the last part (2026-10-20: R4c-2's
two repositories went up in parts this way, and the lead's own context stayed free for the review). **Each pull request's description carries
the review record** (ADR 0421 §5), one line the lead writes from the reviewer's approval:
`Reviewed-by: <who@node> at <full sha>` (the issue 1022 note gives its form). mesh-delivery reads it at every new
head and puts it in the Approve's ask; a missing line, or one naming another commit, is said first in the ask.
It is never a gate.
**A catalogue pull request that removes a module, or a subject a module names, is also judged by the
controller's unit tests against the catalogue's branch**, because those tests read the catalogue and no check
of the catalogue runs them: on 2026-10-20 mesh-catalog #381 removed the old delivery module, and every
controller check failed afterwards on a test of the catalogue's subjects until issue 1406 removed the name
mesh-issues still listened to. A controller try does not find it, since a check reads the catalogue's main
(also in a delivery group that holds the catalogue's pull request); the lead runs mesh-controller's unit
suite with `MESH_CHECK_BESIDE` naming a directory that holds the catalogue's branch, as mesh-controller's
`internal/beside` says.
2. **The forge's merge checks run the integration tests again, as the gate** (`mesh/merge-gate`, `mesh/repo-check`). No
second review: step 2 reviewed this tree.
3. **A check that fails** goes back to the same reviewer, continued by its id, with the failure, for **one more cycle** of step 2 with
@@ -219,6 +254,11 @@ never into the running one, unless it blocks the running one.
carries an integration test that starts the module whole, its process against the real bus and store, and
shows the milestone's acceptance sentence; the forge's checks run it (ADR 0391 §1, §6). Unit tests alone are not
that proof; the mesh has no end-to-end tests yet, and the lab is not used (the operator, 2026-10-16).
**A new manifest word's test goes through the push that meets it first, not only the verb that fills it.**
On 2026-10-19 release 1 of feature mail-sender tested a process's own bus account through `module issue`
alone; the first push after mesh-mail declared the process composed a random 40-character stand-in for the
account file before anyone issued it, and the process crash-looped until the account was issued and pushed by
hand (issue 1226).
5. **The operator's Approve is asked as soon as the checks pass** (ADR 0421). Nobody marks, labels, comments or
rechecks: mesh-delivery asks the Approve on Telegram, the channel that proves who answered today (ADR 0398 §3;
an Approve at the terminal is issue 953), once both checks passed, for a delivery group once for all its
@@ -248,6 +288,17 @@ delivery: no machine receives it (issue 889 while it still starts one). The lead
naming its pull requests, and removes what is left: no `feat/<slug>` branch and no `.work/<slug>` worktree
survive.
**What a merge moves is read from its plan before the operator is told.** A change to mesh-sdk rebuilds every
module that takes the SDK, with their backups and their asks for a person (issue 721): on 2026-10-20 the lead
said an SDK merge moved nothing, and its delivery plan moved mesh-sdk with 41 dependents to all four machines.
The lead reads the merge gate's plan in `mesh-delivery.show` and names what it moves before asking an Approve.
**A failed plan's Retry is asked of the operator alone, never beside an Approve.** An Approve's merge starts a
plan, and the controller refuses a Retry while another plan runs; on 2026-10-20 the operator chose both at once,
the merge's plan started first, the chosen Retry was refused and nobody asked it again (issue 1408). So the lead
never tells the operator to answer a Retry while an Approve is open, and reads the failed plan again after the
answer: a Retry refused is asked again once the other plan ends.
## Models
| Who | Model | Why |
@@ -271,7 +322,7 @@ on, with when it started and its bound.
|---|---|---|
| a fixing agent's issue | 30 minutes | ask it where it stands; split or take over |
| the review | 15 minutes a round | ask the reviewer; stop at round 3 |
| a merge check | 15 minutes | read the build queue (`mesh-controller.queue`) and the check's log |
| a merge check | 15 minutes | read `mesh-delivery.show` (its state and verdict), then the check's log; the build queue (`mesh-controller.queue`) never lists a running check |
| the Approve's ask | 5 minutes after both checks pass | read `mesh-delivery.show`: an ask not raised is a fault, said to the operator |
| a merge after the Approve | 5 minutes | read `mesh-delivery.show` for the delivery |
| a delivery plan | its class's budget (`mesh-delivery.times`), at most 30 minutes | read `mesh-controller.plans`, then `mesh-delivery.stalled` |
@@ -281,11 +332,20 @@ it, so a check, a merge, a delivery plan, a record's review or the operator's Ap
while the lead's turn has ended wakes nobody: on 2026-10-17 an approved group sat half merged for 14 minutes
unseen (issue 1032). For each wait the lead starts a watcher, an agent on the smaller model that changes nothing,
never the smallest (the operator, 2026-10-17).
It loops `mesh_wait` on the event families of what it watches (`mesh-delivery.transition` and
`mesh-delivery.group` for a delivery, for example) with a timeout under 60 seconds, since the call itself ends near
It loops `mesh_wait` on the event families of what it watches (`mesh-delivery-gate.transition` and
`mesh-delivery-gate.group` for a delivery, for example; the families a watcher may wait on are those the mesh granted
the tool runner, and a refusal names them) with a timeout under 60 seconds, since the call itself ends near
a minute. After every answer or timeout it reads the state from the verb that owns it (`mesh-delivery.show`,
`mesh-controller.plans`, `knowledge-base.get`), because an event only wakes it (ADR 0367). It finishes, and so wakes
the lead, when the state changes or the wait passes its bound. The lead never sleeps or polls by hand to wait.
**A watcher watches one wait and ends with it.** It is never re-pointed at the next pull request or plan: on
2026-10-19 one watcher sent on to three pull requests in turn missed that the third's checks had passed at 04:58,
and the lead told the operator for 15 minutes that they were still running. The next wait gets a fresh watcher,
and before a report the watcher reads the owning verb once more and says only what it says now.
**An ask is said to exist only after it is read on the router.** Before the lead tells the operator that an
Approve, a Retry or a Release waits for them, it reads `operator-channel.asks` and finds that ask open by its key;
on 2026-10-20 an ask was named to the operator that the router did not hold. A merge the gate passed but whose
ask is not on the router is the fault of the Approve's wait in the table above.
**Every ten minutes (the operator, 2026-10-17), every two minutes while a delivery plan runs, and at once when something lands, stops or
needs the operator,** the lead reports, in one of the two layouts below (the operator, 2026-10-16: an overview
+10 -8
View File
@@ -18,8 +18,8 @@ hour is a feature.
## The aim
A small fix spends its time on the fix, and runs the long part once: **one review, one push, one check run,
one delivery**. Everything before the push is local and fast.
A small fix spends its time on the fix, and runs the long part once: **one try before the review, one review, one check run,
one delivery**. Everything before the try is local and fast.
## Steps
@@ -34,20 +34,22 @@ one delivery**. Everything before the push is local and fast.
2. **Move the issue to `fixing`** with `by` as who@node.
3. **Build on a local branch** `fix/<issue>-<slug>`, in a worktree of its own branched from `main`, never in
the repository's own clone, which stays on `main` (playbook [07](07-feature-branches.md), step 0, item 2). Write the
test that fails today first; make the change; keep `gofmt`, `go vet` and the repository's whole unit suite green
test that fails today first, on an assertion about what the code does: a test that fails only because it
does not compile has not failed first (playbook [07](07-feature-branches.md) step 1); make the change; keep `gofmt`, `go vet` and the repository's whole unit suite green
before every commit (each test well under a second; never a merge check or a database locally: how-we-build §5, ADR 0391).
The tests are written from the acceptance criteria, at least one per criterion at its level, each citing the
criterion it proves by its label (`<issue>/AC<n>`, playbook 07 step 0 item 5). Commit, naming the issue. For a playbook or design edit, run the repository's own checks (in hq,
`sh merge-check.sh`).
4. **One review of the local commit, before anything is pushed** (a reviewer that did not write it, larger
`sh merge-check.sh`). Then push the branch and run `mesh-controller.try` on it, so the review reads the
integration tests' real results; a failed try goes back to the same agent before the review.
4. **One review of the commit and its try** (a reviewer that did not write it, larger
model; at most 3 rounds, as playbook 07; from round 2 only what is wrong; it reads each acceptance criterion
against its test, as playbook 07 step 2 item 6; a finding outside the issue's done-when and its acceptance
criteria is opened as an issue of its own, never asked as a change). The reviewer's findings go back to the same agent that wrote the fix, continued by
its id, so the fix keeps its context. A playbook or design edit of a few lines may skip the reviewer: the
operator's Approve of its merge is its review, and the review record says so.
5. **Push once, try it, and open the pull request.** The trunk is what runs and nothing holds merged work back
5. **Push the reviewed head, try it again if the review changed it, and open the pull request.** The trunk is what runs and nothing holds merged work back
(ADR 0401), so a fix waits for no other change's delivery. `mesh-controller.try` on the pushed branch runs
the integration tests once, and the pull request is opened only on a pass; the forge's merge check runs
the integration tests, and the pull request is opened only when the head's last try passed; the forge's merge check runs
them again as the gate (playbook [07](07-feature-branches.md) step 3). They are never run on the machine. A
failed try or check goes back to the same agent with the failure, and the reviewer reads only the change it makes.
6. **The pull request's description carries the review record** (`Reviewed-by: <who@node> at <full sha>`,
@@ -97,7 +99,7 @@ reading itself and uses one subagent only for the review.
## How it is checked
- A small fix has one pull request with one or few commits naming its issue, opened after its review: a pull
request with many heads is a fix reviewed after its push.
request with many heads is a fix reviewed after its pull request was opened.
- Its issue moves to `resolved` within the hour its delivery plan ends.
- A pull request with no review record at its head (the Approve's ask says so first), a fix to a module's
running pushed with no integration test of the module whole, or a review report
+517
View File
@@ -0,0 +1,517 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Novox Playbooks</title>
<style>
:root{
--bg:#f7f6f2; --card:#ffffff; --ink:#1d1f23; --mute:#5d6470; --line:#d9d6cc; --soft:#efede6;
--idea:#2f5fa7; --sym:#b4462b; --dec:#7a4bb0; --build:#3c8a5a; --mesh:#0f6f7a; --op:#c2410c; --side:#8a6d1f;
--ok:#15803d; --okbg:#e6f4ea; --warn:#a16207; --warnbg:#fdf3d7; --bad:#b91c1c; --badbg:#fde8e8;
}
@media (prefers-color-scheme: dark){
:root:not([data-theme="light"]){
--bg:#15171b; --card:#1e2127; --ink:#e8e6e1; --mute:#9aa1ad; --line:#353a43; --soft:#262a31;
--idea:#7aa7ef; --sym:#f08a6c; --dec:#b993ef; --build:#6fcf97; --mesh:#4fc3cf; --op:#fb923c; --side:#d9b44a;
--ok:#4ade80; --okbg:#173222; --warn:#facc15; --warnbg:#3a3016; --bad:#f87171; --badbg:#3a1d1d;
}
}
:root[data-theme="dark"]{
--bg:#15171b; --card:#1e2127; --ink:#e8e6e1; --mute:#9aa1ad; --line:#353a43; --soft:#262a31;
--idea:#7aa7ef; --sym:#f08a6c; --dec:#b993ef; --build:#6fcf97; --mesh:#4fc3cf; --op:#fb923c; --side:#d9b44a;
--ok:#4ade80; --okbg:#173222; --warn:#facc15; --warnbg:#3a3016; --bad:#f87171; --badbg:#3a1d1d;
}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--ink);font:15px/1.55 system-ui,-apple-system,"Segoe UI",sans-serif}
main{max-width:1120px;margin:0 auto;padding:28px 16px 72px}
h1{font-size:28px;margin:0 0 4px}
h2{font-size:20px;margin:44px 0 6px}
h3{font-size:16px;margin:0 0 6px}
p{margin:6px 0 10px}
.sub{color:var(--mute);margin:0 0 18px}
.lead{max-width:760px}
nav{display:flex;flex-wrap:wrap;gap:8px;margin:14px 0 6px}
nav a,.chip{display:inline-block;padding:4px 11px;border:1px solid var(--line);border-radius:999px;color:var(--ink);text-decoration:none;font-size:13px;background:var(--card);cursor:pointer}
nav a:hover,.chip:hover{border-color:var(--mute)}
.chip.on{background:var(--ink);color:var(--bg);border-color:var(--ink)}
.box{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:16px;margin:12px 0}
.scroll{overflow-x:auto}
svg{display:block;max-width:100%;height:auto;font-family:inherit}
svg text{fill:var(--ink);font-size:13px}
svg .mute{fill:var(--mute);font-size:11.5px}
svg .t{font-weight:600}
svg .edge{stroke:var(--mute);stroke-width:1.6;fill:none}
svg .dash{stroke-dasharray:5 4}
.legend{display:flex;flex-wrap:wrap;gap:14px;font-size:13px;color:var(--mute);margin-top:8px}
.legend i{display:inline-block;width:12px;height:12px;border-radius:3px;margin-right:6px;vertical-align:-1px}
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:14px}
.pb{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:14px 16px;border-top:4px solid var(--c,var(--line))}
.pb .num{font:600 12px/1 ui-monospace,monospace;color:var(--c);letter-spacing:.04em}
.pb h3{margin:4px 0 4px;font-size:17px}
.pb .trig{color:var(--mute);font-size:13.5px;margin:0 0 8px}
.pb details{border-top:1px solid var(--line);padding-top:6px;margin-top:6px}
.pb summary{cursor:pointer;font-size:13.5px;font-weight:600;color:var(--ink);list-style:none}
.pb summary::before{content:"▸ ";color:var(--mute)}
.pb details[open] summary::before{content:"▾ "}
.pb ol,.pb ul{margin:6px 0 4px;padding-left:20px;font-size:13.5px}
.pb li{margin:2px 0}
.kv{display:grid;grid-template-columns:96px 1fr;gap:4px 10px;font-size:13px;margin-top:6px}
.kv b{color:var(--mute);font-weight:500}
.tag{display:inline-block;font-size:11.5px;padding:1px 7px;border-radius:999px;background:var(--soft);color:var(--mute);margin:0 4px 4px 0}
.new{background:var(--okbg);color:var(--ok);font-weight:600}
table{border-collapse:collapse;width:100%;font-size:14px}
th,td{text-align:left;padding:7px 9px;border-bottom:1px solid var(--line);vertical-align:top}
th{color:var(--mute);font-weight:600;font-size:12.5px;text-transform:uppercase;letter-spacing:.03em}
.note{border-left:3px solid var(--ok);background:var(--okbg);padding:9px 12px;border-radius:6px;font-size:14px}
.two{display:grid;grid-template-columns:1fr 1fr;gap:14px}
@media (max-width:760px){.two{grid-template-columns:1fr}}
footer{color:var(--mute);font-size:13px;margin-top:48px}
code{font:12.5px ui-monospace,monospace;background:var(--soft);padding:1px 5px;border-radius:4px}
.theme{float:right}
</style>
</head>
<body>
<main>
<button class="chip theme" id="theme" type="button">Theme</button>
<h1>Novox Playbooks</h1>
<p class="sub">How work is done on the mesh, from an idea or a symptom to a delivered change. Drawn from hq's <code>00-META/process/</code> at main <code>f269e83</code>, 2026-10-19, including playbook change hq #433 (try before the review).</p>
<nav>
<a href="#cycle">The cycle</a>
<a href="#feature">A feature, step by step</a>
<a href="#tests">When tests run</a>
<a href="#review">The review loop</a>
<a href="#issues">An issue's life</a>
<a href="#where">Where things live</a>
<a href="#all">All playbooks</a>
</nav>
<!-- ============ THE CYCLE ============ -->
<h2 id="cycle">The development cycle</h2>
<p class="lead">Work enters as an <b>idea</b> or a <b>symptom</b>. An idea becomes a decision, a design and then milestones; a symptom becomes an issue. Both end as code delivered to the machines, and the as-is design is updated. <b>No design without a decision; no development without a design or an issue that sends it.</b></p>
<div class="box scroll">
<svg viewBox="0 0 1080 470" role="img" aria-label="The development cycle across playbooks 01 to 09">
<defs>
<marker id="ar" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0L10,5L0,10z" fill="var(--mute)"/></marker>
</defs>
<!-- entry points -->
<g>
<rect x="16" y="40" width="120" height="50" rx="25" fill="none" stroke="var(--idea)" stroke-width="2"/>
<text x="76" y="70" text-anchor="middle" class="t">Idea</text>
<rect x="16" y="300" width="120" height="50" rx="25" fill="none" stroke="var(--sym)" stroke-width="2"/>
<text x="76" y="330" text-anchor="middle" class="t">Symptom</text>
</g>
<!-- idea path -->
<g>
<rect x="176" y="30" width="150" height="70" rx="10" fill="var(--card)" stroke="var(--idea)" stroke-width="2"/>
<text x="251" y="58" text-anchor="middle" class="t">01 Research</text>
<text x="251" y="78" text-anchor="middle" class="mute">questions, findings,</text>
<text x="251" y="92" text-anchor="middle" class="mute">direction confirmed</text>
<rect x="366" y="30" width="160" height="70" rx="10" fill="var(--card)" stroke="var(--dec)" stroke-width="2"/>
<text x="446" y="58" text-anchor="middle" class="t">02 Graduation</text>
<text x="446" y="78" text-anchor="middle" class="mute">decisions in the</text>
<text x="446" y="92" text-anchor="middle" class="mute">knowledge base · to-be</text>
<rect x="566" y="30" width="160" height="70" rx="10" fill="var(--card)" stroke="var(--dec)" stroke-width="2"/>
<text x="646" y="58" text-anchor="middle" class="t">04 Breakdown</text>
<text x="646" y="78" text-anchor="middle" class="mute">milestones by release,</text>
<text x="646" y="92" text-anchor="middle" class="mute">issues per repository</text>
<rect x="766" y="30" width="150" height="70" rx="10" fill="var(--card)" stroke="var(--build)" stroke-width="2.5"/>
<text x="841" y="58" text-anchor="middle" class="t">07 Feature</text>
<text x="841" y="78" text-anchor="middle" class="mute">one milestone =</text>
<text x="841" y="92" text-anchor="middle" class="mute">one release</text>
</g>
<!-- symptom path -->
<g>
<rect x="176" y="290" width="150" height="70" rx="10" fill="var(--card)" stroke="var(--sym)" stroke-width="2"/>
<text x="251" y="318" text-anchor="middle" class="t">03 Issues</text>
<text x="251" y="338" text-anchor="middle" class="mute">opened in the tracker,</text>
<text x="251" y="352" text-anchor="middle" class="mute">diagnosed, located</text>
<rect x="566" y="290" width="160" height="70" rx="10" fill="var(--card)" stroke="var(--build)" stroke-width="2.5"/>
<text x="646" y="318" text-anchor="middle" class="t">08 Small fix</text>
<text x="646" y="338" text-anchor="middle" class="mute">one issue, one repository,</text>
<text x="646" y="352" text-anchor="middle" class="mute">about an hour</text>
</g>
<!-- delivered + as-is -->
<g>
<rect x="950" y="160" width="116" height="74" rx="10" fill="var(--card)" stroke="var(--mesh)" stroke-width="2.5"/>
<text x="1008" y="188" text-anchor="middle" class="t">Delivered</text>
<text x="1008" y="206" text-anchor="middle" class="mute">Approve, merge,</text>
<text x="1008" y="220" text-anchor="middle" class="mute">delivery plan</text>
<rect x="766" y="160" width="150" height="74" rx="10" fill="var(--card)" stroke="var(--dec)" stroke-width="2" stroke-dasharray="5 4"/>
<text x="841" y="188" text-anchor="middle" class="t">As-is updated</text>
<text x="841" y="206" text-anchor="middle" class="mute">02 "when it ships":</text>
<text x="841" y="220" text-anchor="middle" class="mute">to-be → implemented</text>
</g>
<!-- side playbooks -->
<g>
<rect x="176" y="400" width="190" height="56" rx="10" fill="var(--soft)" stroke="var(--side)"/>
<text x="271" y="424" text-anchor="middle" class="t">05 Constitution sync</text>
<text x="271" y="442" text-anchor="middle" class="mute">how-we-build → derived page</text>
<rect x="396" y="400" width="190" height="56" rx="10" fill="var(--soft)" stroke="var(--side)"/>
<text x="491" y="424" text-anchor="middle" class="t">06 Writing a module</text>
<text x="491" y="442" text-anchor="middle" class="mute">what runs today → a module</text>
<rect x="876" y="400" width="190" height="56" rx="10" fill="var(--soft)" stroke="var(--side)"/>
<text x="971" y="424" text-anchor="middle" class="t">09 Cleanup</text>
<text x="971" y="442" text-anchor="middle" class="mute">branches, worktrees, on the word</text>
</g>
<!-- edges -->
<path class="edge" d="M136,65 H174" marker-end="url(#ar)"/>
<path class="edge" d="M326,65 H364" marker-end="url(#ar)"/>
<path class="edge" d="M526,65 H564" marker-end="url(#ar)"/>
<path class="edge" d="M726,65 H764" marker-end="url(#ar)"/>
<path class="edge" d="M916,65 C990,65 1008,100 1008,158" marker-end="url(#ar)"/>
<path class="edge" d="M136,325 H174" marker-end="url(#ar)"/>
<path class="edge" d="M326,325 H564" marker-end="url(#ar)"/>
<text x="445" y="317" text-anchor="middle" class="mute">a fix of about an hour</text>
<path class="edge" d="M726,325 C990,325 1008,290 1008,236" marker-end="url(#ar)"/>
<path class="edge" d="M948,197 H918" marker-end="url(#ar)"/>
<path class="edge dash" d="M251,290 C251,200 420,170 440,102" marker-end="url(#ar)"/>
<text x="300" y="196" class="mute">a design gap: decision first</text>
<path class="edge dash" d="M326,300 C560,250 720,170 800,102" marker-end="url(#ar)"/>
<text x="612" y="236" class="mute">larger than an hour: a feature</text>
<path class="edge dash" d="M1008,236 C1008,330 990,360 971,398" marker-end="url(#ar)"/>
</svg>
<div class="legend">
<span><i style="background:var(--idea)"></i>from an idea</span>
<span><i style="background:var(--sym)"></i>from a symptom</span>
<span><i style="background:var(--dec)"></i>decisions and designs</span>
<span><i style="background:var(--build)"></i>code changes</span>
<span><i style="background:var(--mesh)"></i>the mesh delivers</span>
<span><i style="background:var(--side)"></i>alongside</span>
</div>
</div>
<!-- ============ FEATURE SWIMLANE ============ -->
<h2 id="feature">A feature, step by step (playbook 07)</h2>
<p class="lead">One milestone is one release: one branch name in every repository it touches, one worktree per repository, one fixing agent per repository kept for the whole milestone, one reviewer, and one pull request per repository, opened once. Since hq #433 the branches are <b>tried before the review</b>, so the reviewer reads real integration results.</p>
<div class="box scroll">
<svg viewBox="0 0 1080 430" role="img" aria-label="Playbook 07 in lanes: lead, fixing agents, reviewer, the mesh, the operator">
<defs>
<marker id="ar2" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0L10,5L0,10z" fill="var(--mute)"/></marker>
</defs>
<!-- step columns -->
<g class="mute">
<text x="190" y="20" text-anchor="middle" class="t" style="fill:var(--mute)">0 · Prepare</text>
<text x="190" y="34" text-anchor="middle" class="mute">15 min</text>
<text x="410" y="20" text-anchor="middle" class="t" style="fill:var(--mute)">1 · Develop + try</text>
<text x="410" y="34" text-anchor="middle" class="mute">30 min per issue · one try</text>
<text x="630" y="20" text-anchor="middle" class="t" style="fill:var(--mute)">2 · Review</text>
<text x="630" y="34" text-anchor="middle" class="mute">≤ 3 rounds · 15 min</text>
<text x="830" y="20" text-anchor="middle" class="t" style="fill:var(--mute)">3 · Check</text>
<text x="830" y="34" text-anchor="middle" class="mute">≤ 1 more try · 1 check</text>
<text x="1000" y="20" text-anchor="middle" class="t" style="fill:var(--mute)">4 · Deliver</text>
<text x="1000" y="34" text-anchor="middle" class="mute">its delivery plan</text>
</g>
<!-- lanes -->
<g>
<rect x="0" y="46" width="1080" height="72" fill="var(--soft)" opacity=".55"/>
<rect x="0" y="190" width="1080" height="72" fill="var(--soft)" opacity=".55"/>
<rect x="0" y="334" width="1080" height="72" fill="var(--soft)" opacity=".55"/>
<text x="10" y="86" class="t">Lead</text><text x="10" y="102" class="mute">larger model</text>
<text x="10" y="158" class="t">Fixing agents</text><text x="10" y="174" class="mute">smaller model</text>
<text x="10" y="230" class="t">Reviewer</text><text x="10" y="246" class="mute">larger model</text>
<text x="10" y="302" class="t">The mesh</text><text x="10" y="318" class="mute">try, checks, plan</text>
<text x="10" y="374" class="t">Operator</text><text x="10" y="390" class="mute">Telegram</text>
</g>
<!-- lead -->
<rect x="110" y="56" width="160" height="52" rx="8" fill="var(--card)" stroke="var(--dec)" stroke-width="2"/>
<text x="190" y="78" text-anchor="middle" class="t">Survey · criteria</text>
<text x="190" y="96" text-anchor="middle" class="mute">context file per repo</text>
<rect x="340" y="56" width="140" height="52" rx="8" fill="var(--card)" stroke="var(--dec)" stroke-width="2"/>
<text x="410" y="78" text-anchor="middle" class="t">Up to trunk · push</text>
<text x="410" y="96" text-anchor="middle" class="mute">each branch</text>
<rect x="760" y="56" width="140" height="52" rx="8" fill="var(--card)" stroke="var(--dec)" stroke-width="2"/>
<text x="830" y="78" text-anchor="middle" class="t">Push reviewed heads</text>
<text x="830" y="96" text-anchor="middle" class="mute">open the PRs together</text>
<rect x="930" y="56" width="140" height="52" rx="8" fill="var(--card)" stroke="var(--dec)" stroke-width="2"/>
<text x="1000" y="78" text-anchor="middle" class="t">Follow to done</text>
<text x="1000" y="96" text-anchor="middle" class="mute">resolve the issues</text>
<!-- fixing -->
<rect x="300" y="128" width="220" height="52" rx="8" fill="var(--card)" stroke="var(--build)" stroke-width="2"/>
<text x="410" y="150" text-anchor="middle" class="t">Build each issue, commit</text>
<text x="410" y="168" text-anchor="middle" class="mute">unit suite before every commit</text>
<rect x="560" y="128" width="140" height="52" rx="8" fill="var(--card)" stroke="var(--build)" stroke-width="2"/>
<text x="630" y="150" text-anchor="middle" class="t">Fix findings</text>
<text x="630" y="168" text-anchor="middle" class="mute">the same agents</text>
<!-- reviewer -->
<rect x="560" y="200" width="140" height="52" rx="8" fill="var(--card)" stroke="var(--idea)" stroke-width="2"/>
<text x="630" y="222" text-anchor="middle" class="t">Read all repos</text>
<text x="630" y="240" text-anchor="middle" class="mute">+ tries · every criterion</text>
<!-- mesh -->
<rect x="340" y="272" width="140" height="52" rx="8" fill="var(--card)" stroke="var(--mesh)" stroke-width="2"/>
<text x="410" y="294" text-anchor="middle" class="t">try</text>
<text x="410" y="312" text-anchor="middle" class="mute">integration tests run</text>
<rect x="760" y="272" width="140" height="52" rx="8" fill="var(--card)" stroke="var(--mesh)" stroke-width="2"/>
<text x="830" y="294" text-anchor="middle" class="t">Forge checks</text>
<text x="830" y="312" text-anchor="middle" class="mute">gate · repo · group</text>
<rect x="930" y="272" width="140" height="52" rx="8" fill="var(--card)" stroke="var(--mesh)" stroke-width="2"/>
<text x="1000" y="294" text-anchor="middle" class="t">Merge · delivery plan</text>
<text x="1000" y="312" text-anchor="middle" class="mute">backups first, tier by tier</text>
<!-- operator -->
<rect x="860" y="344" width="140" height="52" rx="8" fill="var(--card)" stroke="var(--op)" stroke-width="2.5"/>
<text x="930" y="366" text-anchor="middle" class="t">Approve</text>
<text x="930" y="384" text-anchor="middle" class="mute">once, for the group</text>
<!-- edges -->
<path class="edge" d="M190,108 C190,140 260,154 298,154" marker-end="url(#ar2)"/>
<path class="edge" d="M410,128 V110" marker-end="url(#ar2)"/>
<path class="edge" d="M480,90 C545,110 545,250 462,270" marker-end="url(#ar2)"/>
<path class="edge" d="M480,298 C530,298 540,226 558,226" marker-end="url(#ar2)"/>
<text x="490" y="262" class="mute">results</text>
<path class="edge" d="M630,200 V182" marker-end="url(#ar2)"/>
<path class="edge dash" d="M610,182 V198" marker-end="url(#ar2)"/>
<text x="514" y="196" class="mute">round n+1</text>
<path class="edge" d="M700,226 C740,226 740,82 758,82" marker-end="url(#ar2)"/>
<text x="712" y="140" class="mute">approved</text>
<path class="edge" d="M830,108 V270" marker-end="url(#ar2)"/>
<path class="edge" d="M900,298 C915,298 920,330 925,342" marker-end="url(#ar2)"/>
<path class="edge" d="M1000,344 V326" marker-end="url(#ar2)"/>
<path class="edge" d="M1000,272 V110" marker-end="url(#ar2)"/>
</svg>
</div>
<div class="two">
<div class="box">
<h3>Step 0 · what the lead writes before any agent starts</h3>
<ul style="margin:6px 0;padding-left:20px;font-size:14px">
<li>Every open question settled: on the issue, in the knowledge base, or by the operator.</li>
<li>Into each issue: every file and test it touches (file:line), how it is checked and delivered, a group's merge order.</li>
<li><span class="tag new">new</span>Every other reader and writer of each value the change touches, each with a criterion.</li>
<li>Acceptance criteria: given / when / then, each naming its test level.</li>
<li>Release orders hidden in the milestone, said to the operator, never built around.</li>
<li>Fixtures copied from what the other side really writes, never from memory.</li>
<li>An issue sized over an hour is split before work starts.</li>
</ul>
</div>
<div class="box">
<h3>Step 1 · the fixing agent's brief</h3>
<ul style="margin:6px 0;padding-left:20px;font-size:14px">
<li>One commit per issue, naming it; tests cite their criterion (<code>// 1217/AC3</code>).</li>
<li><span class="tag new">new</span>A test fails first on what the code does. A compile error is no failure: take out the change's effect and see an assertion fail.</li>
<li>The whole unit suite before every commit; each test well under a second.</li>
<li>Integration tests are compiled only: never run on the machine.</li>
<li>Never push, open or merge; never move an issue; never ssh.</li>
<li>Code that differs from the context file: stop and report, file:line.</li>
</ul>
</div>
</div>
<!-- ============ TESTS ============ -->
<h2 id="tests">When tests run</h2>
<p class="lead">Two levels. Unit tests run all the time on the machine; integration tests (a bus, a store or a process started) never run there: <code>mesh-controller.try</code> runs them on the pushed branch, and the forge's merge check runs them again as the gate.</p>
<div class="box scroll">
<svg viewBox="0 0 1080 170" role="img" aria-label="Test levels over a feature's timeline">
<defs><marker id="ar3" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0L10,5L0,10z" fill="var(--mute)"/></marker></defs>
<path class="edge" d="M120,140 H1060" marker-end="url(#ar3)"/>
<text x="120" y="162" class="mute">develop</text><text x="470" y="162" class="mute">end of step 1</text><text x="640" y="162" class="mute">review</text><text x="800" y="162" class="mute">step 3</text><text x="960" y="162" class="mute">merge</text>
<text x="10" y="44" class="t">Unit</text><text x="10" y="60" class="mute">on the machine</text>
<g fill="var(--build)">
<rect x="120" y="36" width="700" height="16" rx="8" opacity=".25"/>
<circle cx="160" cy="44" r="6"/><circle cx="220" cy="44" r="6"/><circle cx="290" cy="44" r="6"/><circle cx="350" cy="44" r="6"/><circle cx="420" cy="44" r="6"/><circle cx="600" cy="44" r="6"/><circle cx="680" cy="44" r="6"/>
</g>
<text x="830" y="42" class="mute">whole suite before every commit</text><text x="830" y="56" class="mute">each test under a second</text>
<text x="10" y="104" class="t">Integration</text><text x="10" y="120" class="mute">never on the machine</text>
<g>
<rect x="120" y="96" width="340" height="16" rx="8" fill="var(--mute)" opacity=".18"/>
<text x="290" y="108" text-anchor="middle" class="mute">compiled only: go vet -tags integration</text>
<rect x="470" y="90" width="110" height="28" rx="6" fill="var(--mesh)"/><text x="525" y="109" text-anchor="middle" style="fill:var(--bg)" class="t">try</text>
<rect x="770" y="90" width="120" height="28" rx="6" fill="none" stroke="var(--mesh)" stroke-width="2" stroke-dasharray="5 4"/><text x="830" y="109" text-anchor="middle" class="mute">try again if changed</text>
<rect x="920" y="90" width="120" height="28" rx="6" fill="var(--mesh)"/><text x="980" y="109" text-anchor="middle" style="fill:var(--bg)" class="t">merge check</text>
</g>
</svg>
</div>
<!-- ============ REVIEW LOOP ============ -->
<h2 id="review">The review loop</h2>
<div class="two">
<div class="box scroll">
<svg viewBox="0 0 520 300" role="img" aria-label="The review loop: at most three rounds">
<defs><marker id="ar4" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0L10,5L0,10z" fill="var(--mute)"/></marker></defs>
<rect x="20" y="20" width="150" height="54" rx="8" fill="var(--card)" stroke="var(--idea)" stroke-width="2"/>
<text x="95" y="44" text-anchor="middle" class="t">Round n of ≤ 3</text><text x="95" y="62" text-anchor="middle" class="mute">code + tries + criteria</text>
<polygon points="300,20 380,47 300,74 220,47" fill="var(--card)" stroke="var(--mute)"/>
<text x="300" y="51" text-anchor="middle" class="t">Wrong?</text>
<rect x="400" y="20" width="110" height="54" rx="8" fill="var(--okbg)" stroke="var(--ok)" stroke-width="2"/>
<text x="455" y="44" text-anchor="middle" class="t">Approve</text><text x="455" y="62" text-anchor="middle" class="mute">Reviewed-by line</text>
<rect x="210" y="130" width="180" height="54" rx="8" fill="var(--card)" stroke="var(--build)" stroke-width="2"/>
<text x="300" y="154" text-anchor="middle" class="t">Same fixing agents fix</text><text x="300" y="172" text-anchor="middle" class="mute">answer per finding</text>
<rect x="20" y="130" width="160" height="54" rx="8" fill="var(--soft)" stroke="var(--side)"/>
<text x="100" y="154" text-anchor="middle" class="t">issue-tracker.open</text><text x="100" y="172" text-anchor="middle" class="mute">anything beyond the issue</text>
<rect x="300" y="236" width="210" height="54" rx="8" fill="var(--warnbg)" stroke="var(--warn)" stroke-width="2"/>
<text x="405" y="260" text-anchor="middle" class="t">After round 3: the operator</text><text x="405" y="278" text-anchor="middle" class="mute">what is left, with a recommendation</text>
<path class="edge" d="M170,47 H218" marker-end="url(#ar4)"/>
<path class="edge" d="M380,47 H398" marker-end="url(#ar4)"/><text x="384" y="38" class="mute">no</text>
<path class="edge" d="M300,74 V128" marker-end="url(#ar4)"/><text x="306" y="104" class="mute">yes</text>
<path class="edge" d="M210,157 C140,157 95,120 95,76" marker-end="url(#ar4)"/><text x="120" y="112" class="mute">n+1</text>
<path class="edge dash" d="M250,66 C200,100 170,120 150,128" marker-end="url(#ar4)"/><text x="178" y="96" class="mute">hardening,</text><text x="178" y="108" class="mute">a nicer form</text>
<path class="edge dash" d="M360,184 C380,210 400,220 405,234" marker-end="url(#ar4)"/>
</svg>
</div>
<div class="box">
<h3>What a round may ask</h3>
<p style="font-size:14px">Only what makes the change <b>wrong</b> against its issues, their acceptance criteria and its decisions. Anything more (hardening, a further case, a nicer form) becomes a new issue, and the round does not wait for it.</p>
<h3 style="margin-top:12px">What every round reads</h3>
<ul style="margin:6px 0;padding-left:20px;font-size:14px">
<li>Every criterion against its test: a test at its level that would fail without the change.</li>
<li>Every new integration test traced through the code, and its try's result.</li>
<li>That each integration test enters by the path the live trigger takes.</li>
</ul>
<div class="note" style="margin-top:10px">Why reviews took more than one round (R3b, 2026-10-19): a test that "failed" only by not compiling, a reader of the changed record the survey missed, and bus tests judged by reasoning before they ran. hq #433 changes all three.</div>
</div>
</div>
<!-- ============ ISSUE LIFE ============ -->
<h2 id="issues">An issue's life (playbook 03)</h2>
<p class="lead">The issue tracker (seat <code>issue-tracker</code>) holds every issue's number, status, owner, fix and links. Its <code>move</code> refuses a wrong move at the write.</p>
<div class="box scroll">
<svg viewBox="0 0 1080 230" role="img" aria-label="Issue statuses and the moves between them">
<defs><marker id="ar5" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0L10,5L0,10z" fill="var(--mute)"/></marker></defs>
<g>
<rect x="20" y="70" width="120" height="50" rx="25" fill="var(--card)" stroke="var(--sym)" stroke-width="2"/><text x="80" y="100" text-anchor="middle" class="t">open</text>
<rect x="220" y="70" width="140" height="50" rx="25" fill="var(--card)" stroke="var(--warn)" stroke-width="2"/><text x="290" y="100" text-anchor="middle" class="t">diagnosing</text>
<rect x="440" y="70" width="120" height="50" rx="25" fill="var(--card)" stroke="var(--dec)" stroke-width="2"/><text x="500" y="100" text-anchor="middle" class="t">located</text>
<rect x="640" y="70" width="120" height="50" rx="25" fill="var(--card)" stroke="var(--build)" stroke-width="2"/><text x="700" y="100" text-anchor="middle" class="t">fixing</text>
<rect x="860" y="70" width="140" height="50" rx="25" fill="var(--okbg)" stroke="var(--ok)" stroke-width="2.5"/><text x="930" y="100" text-anchor="middle" class="t">resolved</text>
<rect x="440" y="170" width="120" height="44" rx="22" fill="var(--soft)" stroke="var(--mute)"/><text x="500" y="197" text-anchor="middle" class="t">wontfix</text>
</g>
<path class="edge" d="M140,95 H218" marker-end="url(#ar5)"/><text x="146" y="86" class="mute">by</text>
<path class="edge" d="M360,95 H438" marker-end="url(#ar5)"/><text x="366" y="86" class="mute">located-in</text>
<path class="edge" d="M560,95 H638" marker-end="url(#ar5)"/><text x="590" y="86" class="mute">by</text>
<path class="edge" d="M760,95 H858" marker-end="url(#ar5)"/><text x="770" y="138" class="mute">fixed-by, delivered,</text><text x="770" y="152" class="mute">proven · core: replay</text>
<path class="edge dash" d="M140,105 C300,150 520,150 640,108" marker-end="url(#ar5)"/><text x="300" y="150" class="mute">straight to fixing, with by</text>
<path class="edge dash" d="M500,120 V168" marker-end="url(#ar5)"/><text x="508" y="150" class="mute">reason</text>
<path class="edge dash" d="M700,70 C700,30 600,30 590,30 C400,30 120,30 90,68" marker-end="url(#ar5)"/><text x="320" y="24" class="mute">stopping: back to open, with a note of branch, last commit and what is left</text>
</svg>
<div class="legend"><span>A worker idle for a day may be taken over without a reason; otherwise <code>take-over</code> with a reason. An issue about a condition is linked to it, so the alert names the issue.</span></div>
</div>
<!-- ============ WHERE THINGS LIVE ============ -->
<h2 id="where">Where things live</h2>
<div class="box scroll">
<table>
<tr><th>What</th><th>Lives in</th><th>Written with</th><th>Never</th></tr>
<tr><td>Decisions (ADRs) and research efforts</td><td>the knowledge base (seat <code>knowledge-base</code>)</td><td><code>reserve</code>, then <code>propose</code> the record whole</td><td>a file in hq</td></tr>
<tr><td>Issues: status, owner, fix, links</td><td>the issue tracker</td><td><code>open</code>, <code>move</code>, <code>note</code>, <code>link</code></td><td>a status key in a report</td></tr>
<tr><td>An issue's narrative (design or governance only)</td><td>hq <code>04-ISSUES/NNN-…/</code></td><td>a pull request</td><td>its status</td></tr>
<tr><td>Designs, as-is and to-be</td><td>hq <code>03-DESIGN/</code>, status in frontmatter</td><td>a pull request</td><td>the two layers mixed</td></tr>
<tr><td>Playbooks, glossary, how-we-build</td><td>hq <code>00-META/</code></td><td>a pull request</td><td>a copy in a session's memory</td></tr>
<tr><td>The rules every session keeps</td><td>instruction sections (module <code>claude-code</code>)</td><td><code>claude_code_instructions_propose</code>, the operator's Approve</td><td>a hand-edited file</td></tr>
</table>
</div>
<!-- ============ ALL PLAYBOOKS ============ -->
<h2 id="all">All playbooks</h2>
<p class="sub">Open a card for its steps, what it produces and how it is checked.</p>
<div id="filters" style="display:flex;flex-wrap:wrap;gap:8px;margin-bottom:12px"></div>
<div class="grid" id="cards"></div>
<footer>Drawn from hq main f269e83 on 2026-10-19 and kept in hq beside the playbooks. The playbooks are the authority; this page is a picture of them, and a change to a playbook's steps updates it in the same pull request (not yet checked: issue 1222).</footer>
</main>
<script>
const P = [
{n:"01",name:"Research",kind:"idea",c:"--idea",
trig:"An idea worth investigating before it is committed to design. A symptom is not research.",
who:"The session that heard the idea leads.",
steps:["Read what is known: ask, get, the tracker's list.","Ask 3 to 5 setup questions, one at a time, each with a recommendation.","Check every option against the mission and how-we-build first.","Reserve a number (kind research), never pick one by hand.","Propose the effort: overview, questions, method, with links.","Research; findings in 04-findings, one version per settled set.","Try at the cheapest level that answers the question.","Write 03-domains: modules, seats, glossary words added or retired.","Write 05-direction: decisions, rejected options, acceptance tests.","Ask the operator to confirm the direction, one point at a time.","If the review asks changes: propose again, at most three times."],
prod:"A research effort in the knowledge base, status active, every version kept. Nothing in hq.",
next:"02",bounds:"Setup 30 min · proposing 15 min · each experiment 30 min · direction 30 min · report every 10 min",
prev:["Research that measured nothing","A recommendation the operator must argue back","Efforts copied into hq"],
check:"propose refuses rule breaks; the knowledge base reviews; records.py on every PR."},
{n:"02",name:"Graduation & design change",kind:"idea",c:"--dec",
trig:"A research effort concludes, or an existing design must change.",
who:"The lead, on the larger model; the decision is recorded before the design moves.",
steps:["Check the conclusion against mission, context and effect.","Reserve a number per decision.","Propose one record per choice that can be reversed on its own.","Pre-check, then propose siblings together.","On changes: revise whole; after three rounds fix findings yourself.","A finding that would change what was decided: ask the operator.","needs-operator: wait for the operator's Approve.","Amend the design, or create a to-be design with status designed.","Add the decisions, updated:, and the effort's glossary words.","Hand the design to the breakdown (04).","Close the effort: graduated with became links, or abandoned.","When it ships: write the as-is, set the to-be implemented."],
prod:"Decision records; a to-be design with frontmatter; glossary entries; later the as-is.",
next:"04",bounds:"One sitting of about an hour",
prev:["A decision rewritten silently","A deferral nothing tracks","A design amended without a decision"],
check:"propose refuses; records.py and index.py; set-wide rules raise conditions."},
{n:"03",name:"Issues",kind:"symptom",c:"--sym",
trig:"Something is wrong: an unenforced rule, a stated behaviour that does not happen, a silent failure.",
who:"Anyone opens; a worker claims it by moving it.",
steps:["Open it in the tracker: title, report; take the number it gives.","Tracker unreachable: take no number, say so.","A design or governance issue: a folder in hq with the report.","About a condition: link the condition's key.","Move to diagnosing with by, before investigating.","Note the trail; a folder issue also gets a dated diagnosis.","Move to located with located-in, a registered owner.","Move to fixing with by, before fixing.","Name fixed-by at the merge; stay fixing until proven live.","Move to resolved; a core issue gives replay or replay-none.","A design gap: run 02 and give amended-design.","Not going to be fixed: wontfix with a reason."],
prod:"A tracker record with notes and links; for a design issue, its narrative folder in hq.",
next:"02 · 07 · 08",bounds:"A worker idle for a day is stale",
prev:["Numbers that collide","Two agents starting the same work","Status held in two places"],
check:"The tracker's move refuses at the write; cycle.py checks the folders."},
{n:"04",name:"Work breakdown & handoff",kind:"idea",c:"--dec",
trig:"A to-be design is settled and its building is about to start.",
who:"The lead who will run the feature, on the larger model.",
steps:["Confirm status designed; each decision traces to a record.","An open question remains: back to research.","Set code: to the owning repositories.","Read the as-is; write it first if it does not say what is replaced.","Write the Milestones: outcome, acceptance tests, repositories.","Cut milestones by release order; fold any not usable alone.","Ask the operator to confirm the milestones.","Open one issue per repository part, linked depends-on.","Write the issue numbers into the design; flip to in-progress.","Hand each milestone to 07, one at a time, one design at a time."],
prod:"Milestones in the design; issues per repository part; the design in-progress.",
next:"07",bounds:"One sitting · one design built at a time · a milestone is one release",
prev:["Building without a breakdown","Building against an undocumented as-is","Deferrals with no owning issue"],
check:"Read by the lead and the reviewer; a check is tracked by issue 845."},
{n:"05",name:"Constitution sync",kind:"side",c:"--side",
trig:"how-we-build.md changed a rule the mesh enforces at runtime.",
who:"Whoever made the change.",
steps:["Edit how-we-build; the reasoning stays there.","Record the change as a decision (02).","Publish the derived page under its slug.","Keep the section numbering stable.","Read the page back; verify the change is there.","Note the sync in the record's consequences."],
prod:"A decision record; the derived constitution page injected into design sessions.",
next:"02",bounds:"no time bound",
prev:["Drift between the reasoned and the enforced copy","A publish that said success and did nothing","Edits to the derived page that vanish"],
check:"The read-back in step 5."},
{n:"06",name:"Writing a module",kind:"side",c:"--side",
trig:"Something that runs today must run on the mesh.",
who:"Whoever ports or writes it.",
steps:["Read what runs: compose file, environment, where data sits.","Name what it provides and requires.","Declare capabilities for machine facts.","Write resources in the order they must happen.","Every secret in a file, never in env.","Pin every image by digest.","New: generate the credential; adopted: accept it.","Prove it against the real software: auth, refusal, restart."],
prod:"A module declaration, optionally a provisioner, and its proof.",
next:"07",bounds:"no time bound",
prev:["A service running with no password","A new password locking out adopted data","A test that cannot fail"],
check:"Example tests refuse the env-file mistake; review."},
{n:"07",name:"Features",kind:"build",c:"--build",
trig:"Work that changes code across issues or repositories, too large for a small fix.",
who:"A lead (larger model), one fixing agent per repository (smaller), one reviewer (larger).",
steps:["Prepare: name the branch, make the worktrees, settle every question.","Write one context file per repository.","Write the findings and acceptance criteria into each issue.","Survey every other reader and writer of each changed value. (new)","Find release orders; copy fixtures from the real writer.","Develop: one agent per repository, one commit per issue.","A test fails first on behaviour, not on compiling. (new)","Bring each branch up to the trunk, push, and try it. (new)","Review all of it with the tries' results; at most 3 rounds.","Push the reviewed heads; try again only what changed.","Open one pull request per repository, together, with Reviewed-by.","The operator's Approve on Telegram; the group merges in one pass.","Follow the delivery plan to done; resolve the issues; clean up."],
prod:"One pull request per repository, one delivery plan, resolved issues, no branch or worktree left.",
next:"02 (when it ships)",bounds:"Prepare 15 min · 30 min per issue · review 15 min, ≤ 3 rounds · one try and one check",
prev:["Pull requests as scratchpads","Context bought again and again","Review rounds spent on what a run would show"],
check:"The forge's merge-gate and repo-check; the review record; the tracker's move."},
{n:"08",name:"Small fixes",kind:"build",c:"--build",
trig:"One issue in one repository, about an hour of work.",
who:"The session, with one reviewer on the larger model.",
steps:["Read the issue and the code; settle any open question.","Write the acceptance criteria.","Move the issue to fixing.","Build on fix/<issue>-<slug> in its own worktree.","A test that fails today first, on behaviour. (new)","Push and try before the review. (new)","One review of the commit and its try.","Push the reviewed head; open the pull request.","The operator's Approve; the merge; the delivery plan.","Prove the fix live and resolve the same hour."],
prod:"One pull request, one delivery, a resolved issue.",
next:"—",bounds:"About an hour · one try before the review, one review, one check",
prev:["A fix reviewed after its pull request opened","A long part run twice","A fix waiting on someone else's delivery"],
check:"The forge's checks; the review record; the tracker's move."},
{n:"09",name:"Cleanup",kind:"side",c:"--side",
trig:"The operator asks, or merged branches, stale pull requests and old worktrees pile up.",
who:"A session on the operator's word; the operator decides every deletion.",
steps:["Take stock, read-only.","Write one report: item, class, evidence.","Classify: safe, likely safe, kept, unknown.","Report stale pull requests; never close them.","Ask: delete the safe items as listed?","Delete only the approved list; never force.","Report what was cleaned, kept and unknown."],
prod:"A stock report, the approved deletions, issues for missing tools.",
next:"03",bounds:"A worktree is safe only if untouched for 2 h · a PR is stale after 3 days",
prev:["Deleting unfinished work","Deletion by name or age alone","Missing tools left as session notes"],
check:"The report names the evidence for each deletion."}
];
const kinds={all:"All",idea:"From an idea",symptom:"From a symptom",build:"Code changes",side:"Alongside"};
let current="all";
function esc(s){return s.replace(/&/g,"&amp;").replace(/</g,"&lt;")}
function render(){
const g=document.getElementById("cards"); g.innerHTML="";
P.filter(p=>current==="all"||p.kind===current).forEach(p=>{
const d=document.createElement("article"); d.className="pb"; d.style.setProperty("--c",`var(${p.c})`);
d.innerHTML=`<div class="num">PLAYBOOK ${p.n}</div><h3>${esc(p.name)}</h3><p class="trig">${esc(p.trig)}</p>
<div>${p.bounds.split(" · ").map(b=>`<span class="tag">${esc(b)}</span>`).join("")}</div>
<details><summary>Steps (${p.steps.length})</summary><ol>${p.steps.map(s=>`<li>${esc(s).replace("(new)",'<span class="tag new">new</span>')}</li>`).join("")}</ol></details>
<details><summary>Produces, prevents, checked by</summary><div class="kv">
<b>Who</b><span>${esc(p.who)}</span><b>Produces</b><span>${esc(p.prod)}</span>
<b>Then</b><span>${esc(p.next)}</span><b>Prevents</b><span>${p.prev.map(esc).join("; ")}</span>
<b>Checked by</b><span>${esc(p.check)}</span></div></details>`;
g.appendChild(d);
});
}
const f=document.getElementById("filters");
Object.entries(kinds).forEach(([k,v])=>{const b=document.createElement("button");b.type="button";b.className="chip"+(k===current?" on":"");b.textContent=v;
b.onclick=()=>{current=k;[...f.children].forEach(x=>x.classList.remove("on"));b.classList.add("on");render()};f.appendChild(b)});
render();
const root=document.documentElement;
try{const t=localStorage.getItem("pb-theme");if(t)root.dataset.theme=t}catch(e){}
document.getElementById("theme").onclick=()=>{const dark=root.dataset.theme?root.dataset.theme==="dark":matchMedia("(prefers-color-scheme: dark)").matches;
root.dataset.theme=dark?"light":"dark";try{localStorage.setItem("pb-theme",root.dataset.theme)}catch(e){}};
</script>
</body>
</html>
+1
View File
@@ -5,6 +5,7 @@ code: [mesh-controller internal/catalogue/state.go, mesh-controller internal/bro
updated: 2026-10-04
decisions:
- "ADR 0201"
- "ADR 0439"
---
# A module's state, as it runs
+177
View File
@@ -0,0 +1,177 @@
---
layer: as-is
status: implemented
code: [mesh-catalog modules/mesh-mail, mesh-catalog modules/mesh-mail-web, mesh-catalog modules/mesh-mail-notifier]
updated: 2026-10-20
decisions:
- "ADR 0359"
- "ADR 0361"
- "ADR 0378"
- "ADR 0379"
- "ADR 0436"
- "ADR 0439"
- "ADR 0440"
---
# The mail modules, as they run
**One module, `mesh-mail`, holds every mail account and is the mesh's one `mail-store`. A second module,
`mesh-mail-web`, is the mesh's one mail web client. A third, `mesh-mail-notifier`, shows a
desktop notice of new mail on the machines it is assigned to. A caller may read an account's mail, or ask to
send from it, only when `mesh-mail`'s trusted setting `grants` names it for that account — one list per
account, giving both at once, to the same callers. A send is never performed by a tool: it is queued for a
process of its own, which sends only once the operator approves it on Telegram.** Read from `mesh-catalog` on
`origin/main` at `a67f0196` (2026-10-20) and from the live mesh through the mesh MCP server on 2026-10-20.
```
mesh-mail (mail-store, once for the mesh — the control-node)
accounts search read send send-status ── MAIL stream (headers only, 7 days) ──▶ a granted caller
own database (index) · own object-store bucket (attachments) · three key-value buckets (sends, account state, newest mails)
process keep (IDLE + index) · process watch (mail_health) · process send (mesh-mail-send account; holds the passwords)
▲ grants: one list per account, read AND send alike
│
┌────┴─────────────────────────────┬───────────────────────────────┐
mesh-mail-notifier (mail-notifier, mesh-mail-web (mail-webclient,
once per machine — laptop, work- once for the mesh — the control-node)
station) sign-in (Keycloak) ─ pages ─ send ask ─▶ operator's Approve (Telegram)
node-notifier.send on new inbox mail
```
## 1. The modules and their seats
- **`mesh-mail`** claims `mail-store`, held once for the mesh (`accounts`, `search`, `read`, `send`,
`send-status`). It is assigned today to one machine, the control-node (`module.json`; confirmed live,
2026-10-20). ADR 0378 §4 decided to move it to the home server as a step of its own, on the operator's
Approve (issue 803); as built, that move has not happened: the live mesh still runs it, and `mesh-mail-web`
beside it, on the control-node.
- **`mesh-mail-web`** is a module of its own, with its own Go module and source under `modules/mesh-mail-web`;
it claims `mail-webclient`, held once for the mesh. It is assigned to the same machine as `mesh-mail` and runs as its
own account, `mesh-mail-web`. Its manifest declares no database and no store of its own: nothing beyond its
route, its sign-in binding and its own settings on disk.
- **`mesh-mail-notifier`** is a module of its own as well (`modules/mesh-mail-notifier`), and keeps a
byte-identical copy of `mesh-mail`'s `mailevent.go`, held to it by its `copies_test.go`, because each module
builds alone. ADR 0378 §9 and ADR 0440 §1 say both are "built from `mesh-mail`'s source"; as built, they are
separate modules that share none of `mesh-mail`'s own code but that copied file. It claims `mail-notifier`, held once per machine; a
module claiming it needs `node-notifier` held on the same machine (ADR 0378 §7). It is assigned today to two
machines, the laptop and the workstation (live read of each machine's modules, 2026-10-20); the control-node
and the home server hold no instance of it.
- None of the three seats is named `mesh-*`. A caller reaches them by seat (`mail-store.<verb>`,
`mail-webclient.<verb>`, `mail-notifier.<verb>`); `mesh-mail` also answers its own tool names (`mail_accounts`
and the rest) through the same handlers.
## 2. Accounts and the MAIL stream
- **Three accounts**, all of the kind `imap`, each a password-based sign-in given at the desk as the module's
own secret `mail-credential-<account>` — one against the mesh's own mail server, two against outside
providers (ADR 0359 §5, ADR 0361 §1; `mesh-controller.settings` for `mesh-mail`, read live on 2026-10-20). No
account's name or address is repeated in this document.
- **It watches each account and says each mail on the bus.** `mesh-mail` keeps IMAP IDLE on the inbox and polls
the other folders; for each new, changed or removed message it publishes an event on its own stream `MAIL` —
account, folder, message id, date, from, to, subject, size, flags and attachment references, never a body or
an attachment's bytes (ADR 0359 §2). The stream keeps 7 days (`module.json`).
- **A send's own record** lives in its state bucket `sends`, which only the `send` process and the tools write
(`module.json`; ADR 0436 §8).
- **The index and the attachment store are its own.** A row per message and folder lives in its own Postgres
database; attachments live in an object-store bucket, each kept once by its SHA-256 digest (ADR 0359 §3, ADR
0439 §5-§6).
- **Two more buckets answer without the database.** An account bucket holds each account's state and, per
folder, its name and its mail and unread counts; a mail bucket holds the newest mails of each account (up to
200, fewer once there are more than 81 accounts), for a view that need not ask the database (ADR 0439 §2-§4).
`mesh-mail-web`'s accounts page reads the folders from this bucket — a read wider than any one caller's grant
— but only for an account its own grant already let it ask about (ADR 0440 §4).
## 3. `grants` — one list per account, giving both at once
- **The setting.** `mesh-mail`'s trusted setting `grants` names, per account, the callers that may reach it: a
module's name alone (granted wherever it runs), `<machine>.<module>`, or a person. Nobody is granted an
account by default (ADR 0359 §4).
- **One check, every door.** The code that decides whether a caller may read an account's mail (`mail_search`,
`mail_read`, the account's share of `MAIL`) and the code that decides whether it may ask to send from it
(`mail_send`) is the same function, reading the one list (`modules/mesh-mail/cmd/mesh-mail/config.go`'s
`Grants.Allows`, called from `tools.go` for every tool, `mail_send` included, confirmed on `origin/main` at `a67f0196`). There is no
second list and no narrower grant for sending alone: a caller named for an account may do both, and a caller
left out of it may do neither.
- **Who is granted, today, for every account alike** (`mesh-controller.settings` for `mesh-mail`, read live on
2026-10-20): `mesh-mail-notifier` (named alone, so wherever it is assigned it may read); each of the four
machines' own `node-tools` — the control-node's, the laptop's, the workstation's and the home server's, which
is how an agent session on any of them reaches a mail account at all; and the control-node's `mesh-mail-web`.
So the notifier, every machine's agent sessions, and the web client can each both read an account's mail and
ask to send from it, from the one list.
## 4. The send path
- **A send is an ask, never a write by the tool.** `mail_send` (account, to, subject, body) is open only to a
caller the account's `grants` names. It performs nothing itself: it raises an ask on the operator channel
naming the account, the recipients, the subject and the first lines, and returns the ask's id at once (ADR
0361 §2).
- **The operator's Approve queues it once.** On an Approve recorded on a channel that proves who answered,
`mesh-mail`'s tools record the send `approved` and publish it, once, on the work queue `mail-sends`, carrying
only the send's id, its account and its ask id — never the recipients, subject or body, which stay in the
send's own record (ADR 0436 §8).
- **A process of its own performs it.** `mesh-mail`'s process `send` runs as its own account, `mesh-mail-send`,
never the tool runner's account: it alone reads the accounts' passwords. It takes work from the queue,
verifies the operator's warrant again against the router's own record rather than trusting the queued message,
claims the send once, hands it to the account's outgoing server, and records the outcome — retrying a send the
server provably never took, up to five times, each after a longer wait, before giving up (ADR 0436 §2, §7,
§9-§10).
- **`mail_send_status` answers one of a fixed set of states**: waiting for the operator, approved and waiting
for the sender, being sent, waiting for its next attempt, sent, failed (with its reason), refused, not proven
(the handover may or may not have reached the server), declined, expired, or unknown for an id it does not
know or that belongs to another caller's send (`send.go`, `sender.go`; ADR 0436 §12).
## 5. `mesh-mail-web`
- **Its pages**, all behind sign-in: the accounts page (`/`), a folder (`/folder`), search (`/search`), one
mail (`/mail`), an attachment download (`/attachment`), writing a mail (`/write`), its send (`/send`) and its
state (`/send-status`) — plus the sign-in callback (`/signed-in`). Every changing form (today, the send)
carries a token bound to the session (`server.go`, `csrf.go`).
- **Sign-in is Keycloak's.** The module requires `oidc-client` and uses the authorisation-code flow with PKCE;
the identity (the ID token's own username claim) must be in the trusted setting `operators` — nobody by
default, and today one identity, the operator's own (ADR 0440 §3; `mesh-controller.settings` for
`mesh-mail-web`, read live on 2026-10-20). The session cookie is `Secure`, `HttpOnly` and `SameSite=Strict`,
ending 12 hours after sign-in (`auth.go`).
- **Reachable only from the mesh's own machines.** Its route's reach is set to `internal`, so the route proxy
composes only its internal name, never a public one, and the module itself answers any other host 404 (ADR
0440 §2; `server.go`).
- **A mail is shown as text.** `mail_read`'s own HTML-to-text is rendered through `html/template`'s escaping,
never parsed as markup; an attachment is answered only as a download, `Content-Disposition: attachment` and
`X-Content-Type-Options: nosniff`; every page carries a content security policy allowing nothing from
elsewhere (ADR 0440 §5; `mail.go`).
- **It writes nothing of its own.** No database, nothing on disk beyond its own settings; it reaches mail only
through `mail-store`'s verbs, calling as `<machine>.mesh-mail-web`, and reads one thing directly besides —
`mesh-mail`'s account bucket, for folders and counts, of an account its own grant already let it ask about
(ADR 0440 §4, §6).
## 6. `mesh-mail-notifier`
- **One notice per new inbox mail, at most once.** For each account it is granted, on hearing `mesh-mail`'s
`added` event, it shows the sender (name and address), the subject and the account's name through the
machine's `node-notifier`, under its own account (`runs-as`), never the tool runner's.
- **Quiet rules.** It shows nothing for a mail outside the inbox, for a mail from the account's own address, and
for a mail already flagged read when it arrives (`notifier.go`, `mailevent.go`). During an account's first sync
or a resync `mesh-mail` publishes no `added` events, so nothing reaches the notifier then (`notifier.go`'s
opening note, citing `mesh-mail`'s `imap.go`).
- **Folding.** More than three qualifying mails of one account within a trailing 60-second window become one
notice, shown once the window has passed since the fold's own first mail, naming how many and the latest
sender; a message already shown — tracked by its message id, or by its folder, UIDVALIDITY and UID where it
has none — is never shown twice, even redelivered. A notice that fails to send is retried at most once a
minute (`notifier.go`, its constants `FoldOver`, `Window`, `RetryBackoff`).
- **Assignment.** It is assigned today to the laptop and the workstation (live read, 2026-10-20), where it needs
`node-notifier` to be held beneath it.
## 7. Health
- **`mesh-mail`**'s liveness is its `keep` process's own unit health; its watching is judged once a minute by
its `watch` process and read through the tool `mail_health`: `waits` (naming the secret) for an account whose
password was never given, `unhealthy` (naming the account and why) for a connection down or an IDLE silent
for five minutes, healthy otherwise; its `send` process is a plain unit (ADR 0359 §6; `health.go`).
- **`mesh-mail-web`**'s own tool `mail_web_health` reads the one state its HTTP endpoint keeps: still setting up
sign-in, listening, or ended with its error — unhealthy whenever it is not listening (`main.go`).
- **`mesh-mail-notifier`** declares no process resource: its manifest declares only its account and its state
directory. Its bundle is the one process the machine's runtime launches for it and keeps alive, under its own
account (`runs-as`); it serves its tools and runs the watcher beside them (`main.go`'s opening note; commit
12d78520, issue 859). Its tool `mail_notifier_health` is unhealthy once an account's notice
has failed to show for more than ten minutes, and answers only when asked; no check of the node-engine judges
the module by it. A watching process of its own is tracked by issue 841, not yet decided (its `README.md`).
The comment in its `health.go` that still names a process `notify` predates that commit. It keeps one state
bucket, `shown`, for 30 days, to show no mail twice (`module.json`).
+1
View File
@@ -26,6 +26,7 @@ Where the two disagree, the implementation wins and the disagreement is stated.
| [`15-the-agent-and-its-licences.md`](15-the-agent-and-its-licences.md) | The operator's agent on every machine and the licence manager that keeps its licences |
| [`16-logins-on-the-bus.md`](16-logins-on-the-bus.md) | The bus's one list of logins, a login per module assignment, persons limited to their tools, sessions calling through the tool runner's login, and no effect on any tool |
| [`17-accounts-on-a-machine.md`](17-accounts-on-a-machine.md) | The operator and agent accounts on the node record, claude-code's own login and guard, the judgement that the agent account cannot become root, and the forge accounts agents push as |
| [`18-the-mail-modules.md`](18-the-mail-modules.md) | mesh-mail's accounts and MAIL stream, the one `grants` list that gives both reading and sending at once, the send path through its own process and the operator's Approve, mesh-mail-web's pages and sign-in, and mesh-mail-notifier's pop-up and its quiet rules |
## What these documents are not
@@ -2,8 +2,21 @@
layer: to-be
status: in-progress
code: [mesh-controller, mesh-catalog modules/mesh-delivery, mesh-delivery, mesh-host, mesh-sdk]
updated: 2026-10-19
updated: 2026-10-20
decisions:
- "ADR 0469"
- "ADR 0466"
- "ADR 0465"
- "ADR 0463"
- "ADR 0457"
- "ADR 0453"
- "ADR 0452"
- "ADR 0450"
- "ADR 0449"
- "ADR 0447"
- "ADR 0446"
- "ADR 0445"
- "ADR 0444"
- "ADR 0437"
- "ADR 0435"
- "ADR 0434"
@@ -81,15 +94,21 @@ source revision is derived from the keys: every **want** equals its module's **f
forge.
- **The indexer** computes each module's fingerprint at the merge preview and writes the candidates of each
group and the modules of the trunk.
- **The builder** claims a build by creating its key, once per fingerprint, and builds it; build-agent becomes
this part.
- **The checker** runs the merge check on built artifacts.
- **The builder** claims a build by creating its key, once per fingerprint, and builds it; the builder is made in R4c from
build-agent's code, and build-agent builds the builder's own module beside it until R4g, then leaves (ADR 0453 clause 5).
- **The checker** runs the merge check on built artifacts: a change's tests, through the build module of each
module's language and by that language's convention, running only the packages whose inputs changed and
taking the kept verdict of the rest; it writes the change's one status (ADR 0444, ADR 0445, ADR 0447).
- **The gate** merges a group's pull requests once check, review and the operator's Approve agree on the same
heads, and then moves the mesh's source revision in one write.
- **The teller** carries conditions and asks to the operator's phone and is the only writer of an ask's answer.
- **The assigner** writes which fingerprint each node should run, by six rules: the build is done, the
fingerprint is not held, the node is the module's **canary** or the canary runs it healthy, at most `step`
other nodes are ahead when `step` is above zero, every provider runs healthy, and the module's credential for that fingerprint exists.
It also runs the **composition check**: whether the machines a change touches still compose with it, read
from the keys the indexer and the builder hold and from what the controller keeps of machines, settings and
grants, with the composition it sends with (ADR 0446). Its module
is made in R4c for that check alone; its wants come in release 5.
- **The node agent**, the node-engine's own process, applies its wants one module at a time, backs up a data
module first, and writes its haves.
- **The watchman** turns the bus's connections into presence and mirrors every key whose next move a machine
@@ -310,6 +329,59 @@ commit on the SDK's trunk or the head of an SDK pull request the forge holds, op
warrant for those heads in place of an approving forge review (ADR 0421 §9). A member the forge refuses is
merged again on its fresh check while the group's batch stays open for it.
### The check from release 4
*ADR 0444, ADR 0445, ADR 0446, ADR 0447; research 058.*
Until the status switch of release 4 (R4f) is delivered, the subsections above are the rule, with their two statuses.
From its delivery, a check is this:
- **One status per change.** A pull request carries one required status, `mesh/check`, written by the checker
on its head; it passes only when the head merged onto its trunk's head passed its tests and the composition
check. For a member of a delivery group it is the group's one verdict, the same on every member's head: every
member's tests, read beside the others' heads, and one composition of all members' changes. It replaces
`mesh/merge-gate` and `mesh/repo-check`; the forge requires it and the gate reads it from R4f, the delivery in which
the checker's line is the only one (ADR 0447 §5); the checker writes it unrequired from R4c (ADR 0453 clause 4). `mesh/delivery-group` stays the delivery's view, not a required check. Wherever
this design says both checks passed, they read this one status.
- **What the status says.** Its line says the verdict, the tests run and kept at each level, the machines
composed, and how long each part took: waiting in the queue, cloning, the unit level, the integration level's
setup and its running, and the composition. A failure names the test or the machine first. The line links to
the check's full report, which carries the delivery plan's line too (ADR 0447 §3).
- **Tests by the language's convention.** The checker runs a change's tests through the build module of each
module's language (milestone 6's build modules, ADR 0405), with no manifest field and no script per
repository: in Go, `go test` is the unit level and the `integration` build tag the other, so the two levels
are apart by construction; in Node and TypeScript, the type-check and the package's `test` script, and its
`test:integration` script when it has one. A repository with no tests has none, and says so. Each level is
judged by its own bounds: the unit level by its running time (five seconds), the integration level by its
setup (thirty seconds) and its tests' running time (sixty seconds), measured and judged apart; the integration level's bounds fail a repository from its
first check that meets them (ADR 0453 clause 9). A
repository's `merge-check.sh` goes in R4g (ADR 0453 clause 8); hq, which builds no module, keeps its script until issue 1296 settles how
it is checked (ADR 0445).
- **Only what changed runs.** The build module names, from the toolchain, everything each package's tests read,
and the checker keeps a package's passing verdict at each level under the digest of those inputs. A check
runs a package only when no passing verdict is kept under its digest; a check asked again after a failure
runs what failed and what changed, never the whole suite; the change's own new and changed tests run first,
and the check stops at their first failure, before the rest and before the composition. A kept verdict is
written once, by the checker's credential alone, from the result it read, and is addressed by its digest;
each check builds with a cache of its own, so one pull request cannot poison another's verdict (ADR 0444,
issue 482). With every check asked again on a trunk move (ADR 0395), such a re-ask runs only what the move
changed.
- **The composition check is the assigner's.** Whether the machines still compose with a change is checked by
the part that owns what each machine should run. The assigner reads the modules, fingerprints and manifests
the indexer holds and the builds the builder holds; the machines, their roles, assignments and settings, and
the grants and credentials it reads from the controller's facts snapshot and from the controller's buckets it
names in its manifest's `reads` (ADR 0237 §1, ADR 0408 §3). It clones no repository but at most the change's
own, and
composes only the machines that run or could run a module the change moves or adds, or whose grants or
providers it alters; for a group, once over every member's change, read from the indexer's candidates of the
group, and while the indexer holds none the group's check waits and says why. It composes with the function the
controller sends with, moved into mesh-sdk, so the two agree for one mesh-sdk version; its verdict names the
mesh-sdk version it composed with, and while the assigner and the controller run different versions the
controller's self-check raises a condition until they agree. No judge is built from a change: a change to the
composition is proved by its own tests, a field only a newer composition reads goes out as optional first, and
the running composition refuses a manifest that uses a field it does not read, naming the field. What it fails on, and its warning past twelve rebuilt modules, are today's merge
gate's (ADR 0446, ADR 0237 §2).
## The Approve and the merge
*ADR 0421, ADR 0398 §3, ADR 0406 §1.*
@@ -447,11 +519,13 @@ delivery time.
image and its module retire.
- **Every builder runs the same toolchain, and says so.** Each builder names its toolchain versions with every
build outcome, and the controller's self-check raises a condition while two builders differ.
- **Builds and tests run directly**, as the build seat's own account on its node, in no container. A
repository's `# mesh-check-toolchain` line selects which installed toolchain runs its check.
- **A judge has an account of its own.** A judge built from a sibling's merge (ADR 0382 §3) is built and run as a
second account of the Go build module, whose home, configuration and cache the build seat's account cannot write,
so a check's script cannot change it.
- **Builds and tests run directly**, as the build seat's own account on its node, in no container. The
build module of each module's language runs its tests by that language's convention; no repository's
script or toolchain line decides it, hq's alone excepted until issue 1296 (ADR 0445).
- **A judge has an account of its own, until release 4 is delivered (R4f).** Until then, a judge built from a sibling's
merge (ADR 0382 §3) is built and run as a second account of the Go build module, whose home, configuration and
cache the build seat's account cannot write, so a check's script cannot change it. From R4f no judge is
built from a change and the account is not made (ADR 0446 §5).
- **Every dependency comes by the version its lock file names**, checked against its checksum: the mesh's own
code from the mesh's forge, its own packages from its package registry, any other from its public source.
- **A builder fetches from the forge over SSH with a key of its own**, as the build seat's account, under a forge
@@ -587,6 +661,23 @@ From ADR 0429 (research 063's done-when, and the live proof each of its releases
20. Each release's pull request reports the controller's code lines against research 063's baseline (111,386
lines on 2026-10-18).
From ADR 0444 to ADR 0447 (research 058's conclusion: a small fix delivered within four minutes after
development, the check's share about a minute):
21. A leaf fix's check takes about a minute, measured as research 058 measured it: the check's log from its
first line to its verdict, plus the time from the change's announcement to its verdict in the delivery,
each part's time as the status's line says it.
22. A change that adds a failing test is answered failed with only that test's package run; a check asked again
after one failed package runs that package alone; a check whose inputs equal an earlier one's runs nothing
and takes every kept verdict. Measured on the status's line: the tests run and kept at each level, and the
failed test it names.
23. A library or catalogue change's composition check composes only the machines it touches, and clones no
repository but at most the change's own. Measured in the check's report: the machines composed, against the
machines that run or could run a module the change moves or adds, and the repositories cloned.
24. A pull request carries one required status, and a delivery group one verdict on every member's head, its
line naming what was proved and each part's time; no repository a module is built from holds a
`merge-check.sh`.
## What retires
- The "release branches" and the controller's judge of them (ADR 0358, superseded by ADR 0401).
@@ -603,6 +694,11 @@ From ADR 0429 (research 063's done-when, and the live proof each of its releases
group's merges (ADR 0418 §1, §5, §6).
- The controller's tables of builds, build requests, rollouts and their tiers, batches and merges, and
assignments; the record mirrors, probe D17's audit and healer H6 for those kinds (ADR 0408).
- At release 4's deliveries (R4f for the merge gate, the judge and the two statuses; R4g for the scripts): `merge-check.sh` and its `# mesh-check-toolchain` line in every repository a module is
built from, hq's alone kept until issue 1296 (ADR 0445 §6); the controller's merge gate, composing every
machine from clones and throwaway stores (ADR 0446 §1, §6); the judge built from a change and the Go build
module's second account for it (ADR 0446 §5); and the two statuses `mesh/merge-gate` and `mesh/repo-check`
(ADR 0447 §1).
## Milestones
@@ -623,7 +719,7 @@ release reports the controller's code lines against research 063's baseline (acc
| 3 fix | A group approved together merges in one pass and goes out in one delivery plan, also when its members' tests read each other's repositories | 1 | mesh-catalog (mesh-delivery, gitea) | 1082, 1049, 1047, 1083, 1079 (1084 closed as its duplicate) |
| 5 | One read of a module on a node names the commit, build and rollout that put it there | 10, 12 | mesh-controller, mesh-catalog (mesh-config, nats) | 981 |
| 5b | Decided: building and delivering leave the controller for parts that watch and write keys in the repository `mesh-delivery`, landing in six releases (ADR 0429); the forge module makes that repository from its config, protected before any team reaches it (ADR 0428) | — (ADR 0429 names them: 13 to 20) | hq, mesh-catalog (gitea) | 1117, 1124, 1128 |
| 6 | Folded into release 4 by ADR 0429 §10: installed toolchains, builders fetching over SSH with their own key, and an SDK change shipping with its users are built in the builder's new home, so the builder is changed once | (2, 4: proved by release 4) | — | 979, 971 (moved to release 4) |
| 6 | Folded into release 4 by ADR 0429 §10 (the fetch over SSH in R4c, the cut-over in R4e): installed toolchains, builders fetching over SSH with their own key, and an SDK change shipping with its users are built in the builder's new home, so the builder is changed once | (2: proved by R4e; 4: in part by R4e, wholly by R4g) | — | 979, 971 (moved to release 4) |
| R1a | The node-engine, the controller and the parts can read one layout of the have, presence, pulse and stuck keys: mesh-sdk ships it before its users | — (13, 14 need it) | mesh-sdk | 1131 (key layouts) |
| R1b | Every node writes what it runs of each module, on its own keys and nobody else's, and the bus says to one part when a connection opens and closes | 13 in part | the node-engine (`mesh-host`), mesh-controller | 1132 (have keys), 1142 (the node-engine's check, its tests half), 1136 (the have bucket and its grants), 1145 (a module and a build per resource), 1146 (a new repository's first module registered at its merge's Approve), 1147 (the bus's connect events for the watchman), 1160 (a plan passes a machine silent at a middle tier), 1162 (the have bucket takes a node's facts) |
| R1c | The operator sees every module on every node, with its build, phase and health, in `mesh-delivery-cli` (aliased `nox-delivery`) and its web view | 13 | mesh-delivery (view) | 1134 (the view), 1137 (the alias, in the view's manifest) |
@@ -631,21 +727,31 @@ release reports the controller's code lines against research 063's baseline (acc
| R2b | A part, node or check that stops moving is raised as a condition within two minutes, with no timer in a part | 14 | mesh-delivery (watchman), mesh-controller | 1135 (the watchman), 1154 (a bucket whose keys expire), 1155 (running checks readable, with their start), 1156 (stuck keys raised; the silent check from presence), 1157 (a refused send key raised), 1159 (a long check is not stuck) |
| R2c | The controller says when it last heard each machine from presence alone, and no longer hears the alive message | 14 | mesh-controller | 1183 (every reader of last heard takes presence; the controller's alive side deleted) |
| R2d | The node-engine sends no alive message | 14 | the node-engine (`mesh-host`) | 1158 (the node-engine's alive side deleted) |
| R3a | The gate, the bridge, the indexer and the teller can read one layout of the source revision, candidates, modules, merges, asks, answers, owed acts, forge messages and conditions, and one function computes the source fingerprint: mesh-sdk ships them before their users (ADR 0437) | — (15 needs it) | mesh-sdk | opened when it is prepared |
| R3b | The controller finds the delivery seat's holder by its claim and records each build's fingerprint inputs; the router takes conditions on their one subject; the operator sees no change (ADR 0437) | — | mesh-controller, mesh-catalog (messenger) | opened when it is prepared |
| R3c | The forge bridge, the indexer and the teller run beside today's mesh-delivery and write their keys; the view shows modules against the controller's source fingerprints; nothing is merged by them yet (ADR 0437) | 15 in part | mesh-delivery (forge bridge, indexer, teller, view) | opened when it is prepared |
| R3d | A group reaches the mesh as one source revision written by the gate; the merge's Approve is asked by the teller and answered on the phone until issue 953; the view shows the open asks; today's module mesh-delivery is carried over into the gate and ends (ADR 0437) | 15 | mesh-delivery (gate, forge bridge, teller), mesh-catalog (mesh-delivery removed, mesh-issues) | opened when it is prepared |
| R4 | The controller asks no build: a build is keyed by its fingerprint, so a merge builds nothing its check built; with milestone 6, toolchains are installed, builders fetch over SSH, and an SDK change ships with its users | 16, 2, 4 | mesh-delivery (builder, checker), mesh-controller (build dispatch deleted; build-agent leaves), mesh-catalog (build modules, ssh-client, gitea), mesh-sdk | 979, 971; the rest opened when it is prepared |
| R3a | The gate, the bridge, the indexer and the teller can read one layout of the source revision, candidates, modules, merges, asks, answers, owed acts, forge messages and conditions, and one function computes the source fingerprint: mesh-sdk ships them before their users (ADR 0437) | — (15 needs it) | mesh-sdk | 1200 (source revision, candidates, modules, forge messages), 1201 (merges, asks, owed acts, answers), 1202 (a condition on its one subject), 1203 (the source fingerprint, proven against three fingerprints the live builder recorded); ahead of it in mesh-controller, so those could be read: 1204 and 1208 (`builds` says each build's source fingerprint) |
| R3b | The controller finds the delivery seat's holder by its claim and records each build's fingerprint inputs; the router takes conditions on their one subject; the operator sees no change (ADR 0437) | — | mesh-controller, mesh-catalog (messenger) | 1210 (the controller's own path and the merge gate's rule by the seat's claim), 1211 (a merge's announcement heard from the seat's holder), 1212 (the pull requests' keys read from the holder's bucket), 1213 (a merge's warrant read under the asker the announcement names: the teller, or mesh-delivery until R3d), 1214 (the builder computes the source fingerprint with mesh-sdk's function), 1215 (a build record carries its fingerprint's inputs), 1216 (a module may say a condition only under its own name, proven on the controller's grant); in the messenger: 1217 (the router takes a condition on its one subject), 1218 (the same over the bus); one delivery group. Its prepare step found that the controller hears no `transition` or `group` event, and that the router cannot refuse a condition under another module's name, the grant does: both go into R3c's insight on ADR 0437 with 1209 |
| R3c | The forge bridge, the indexer and the teller run beside today's mesh-delivery and write their keys; the view shows modules against the controller's source fingerprints; nothing is merged by them yet (ADR 0437) | 15 in part | mesh-delivery (forge bridge, indexer, teller, view), mesh-catalog (the forge module) | 1235 (the forge bridge writes pull messages on FORGE), 1236 (its merged message, with the trees of the merge commit), 1237 (its messages for the pull requests open at its start and each reconnect), 1238 (the indexer writes the candidate groups), 1239 (the indexer writes the modules' fingerprints at a trunk merge, and 1209: none for an image that compiles a Go program from a context), 1240 (the teller runs and carries nothing yet), 1241 (the view shows the candidate groups and each module's fingerprint against the controller's); in the forge module: 1234 (a merge commit's trees in one call, and the head branch in the open pull requests); one delivery group, mesh-catalog first. Its prepare step found three orders: the forge had no tool for a commit's trees, so the forge module joins the milestone as a member of its group; and mesh-sdk shipped two layouts before it, the controller's build record (1223) and a build's directory and a pull request's module directories (1233). It also found that the bridge's FORGE is a stream the others hear by `consumes`, not a bucket they `read`; that the broker test of the three credentials goes into R3d with the gate's (1243); and that the parts hand-write their bus connection until mesh-sdk gives one (1242): facts for ADR 0437's next insight, with R3d's |
| R3d | The gate's keys can carry what the forge needs and the controller answers a what-if as data, so the gate has what it reads before it runs; a failed delivery plan asks the operator Retry on the phone, and a seat whose holder is assigned nowhere passes to its only claimant; the operator sees only the Retry notice (ADR 0450, ADR 0449) | — | mesh-sdk, mesh-controller | in mesh-sdk: 1321 (a merge key carries the Approve's heads), 1322 (an owed act carries a status's target, a commit without a pull request, two comments), 1323 (a name a key cannot hold is escaped); in mesh-controller: 1324 (what-if as data), 1325 (the build seat reads the gate's pull keys), 1326 (the gate's buckets writable by its runner only), 1243 (the three parts' credentials); and, delivered first as feature no-terminal (ADR 0449): 1317 (the seat passes to its only claimant), 1314 (a failed delivery plan raises a condition), 1315 (Retry offered and performed), 1316 (a refused answer is told). Its prepare step found four things the gate reads that ship before it, which cut the former R3d in two (ADR 0450), and two steps on the path that needed the operator's terminal (ADR 0449) |
| R3e | A group reaches the mesh as one source revision written by the gate; the merge's Approve is asked by the teller and answered on the phone until issue 953; the view shows the open asks; today's module mesh-delivery is carried over into the gate when it first holds the seat and ends; no step needs the operator's terminal (ADR 0437, ADR 0450) | 15 | mesh-delivery (gate's parts: forge bridge, teller, indexer, view), mesh-catalog (the gate, mesh-delivery removed, mesh-issues), mesh-tools (the tool runner's waits) | opened when it is prepared, from R3d's survey; the controller's deletions it makes possible go to release 4 (1318) |
| R4a | The shared layouts of release 4 are on mesh-sdk's main before their users pin them: the catalogue, the validator and the composition over a plain input, the fingerprint's new form, and the key layouts of the builder, the checker, the kept verdicts, the manifests and the candidates; the node-engine reports the user namespaces capability (ADR 0453 clauses 1, 2, 3) | — (2 needs it) | mesh-sdk, the node-engine (`mesh-host`) | in mesh-sdk: 1353 to 1364, 1298 (the composition over a plain input); in the node-engine: 1308 (the user namespaces capability) |
| R4b | mesh-controller and the node-engine use the catalogue, the validator and the composition from mesh-sdk and delete their copies; the controller composes every node both ways on the live facts and the two agree before this is delivered; it writes the grants and credential bindings the assigner reads; its unit level meets its bound (ADR 0453 clauses 1, 3) | — | mesh-controller, the node-engine (`mesh-host`) | 1375 (to be split when it is prepared), 1337 (the unit level's bound) |
| R4c | The new parts run dark beside today's: the indexer writes the manifests by digest and the fingerprints in the new form beside today's (its own, first delivery), then the build modules (the Go build module, the Node and TypeScript build module: toolchain, sandbox, fetch, digest, test, build), the builder, the checker writing `mesh/check` as a status the forge does not require, and the assigner's composition check writing into the checker's report; the build seat's accounts are decided by a record before the build modules' pull requests are opened (ADR 0453 clauses 4, 6, 7, 8, 11, 12, 16; ADR 0465 §4). It is delivered in ADR 0465 §5's order: mesh-sdk's layouts; the forge bridge and the indexer with the controller's facts in format 2; the build modules; the builder; the checker; the bus user list's composer moved into mesh-sdk; the assigner's composition check; the fetch and run steps. The build modules each run as an account of their own with a forge key known read-only on one forge user (ADR 0466); each names the one toolchain version it builds with and refuses on a node that has another, answers a long step with its id and finishes it on a key, is called on a named machine, and runs on the four nodes that run the build agent today (ADR 0469) | 22, 23 (measured on the dark run, proved in R4f) | mesh-delivery (indexer, builder, checker, assigner's composition check), mesh-catalog (build modules, ssh-client, gitea), mesh-controller (the build agent's whole-check tests, the script's steps as tests), mesh-sdk | 971 (its record first), 1297 (the assigner's composition check), 1309 (the fetch and run steps), 1287, 1288, 1289, 1290, 1291, 1292 (the checker; the build modules run the tests), 1251; the steps of 1293 and 1294 that are no test become tests in R4c to R4e; the rest opened when it is prepared |
| R4d | Four modules stop being built from a Dockerfile: route-proxy, nats, n8n and mail's autoconfiguration module (automx, replaced by a Go program), each rework its own release step, nats in a delivery of its own; free between R4a and R4e and never in the same delivery as another release (ADR 0452, ADR 0453 clauses 13, 14, 16) | — | mesh-catalog, mesh-controller (route-proxy), the node-engine (`mesh-host`: n8n's supplementary group) | the four image reworks, opened when it is prepared |
| R4e | The controller asks no build: a build is keyed by its fingerprint, the new form becomes the only one as one group, and a merge builds nothing its check built; builders fetch over SSH, so an SDK change ships with its users; the vendored copies leave mesh-controller and mesh-sdk; delivered only once the builder's artifact digests equal build-agent's (ADR 0453 clauses 2, 4, 5, 6) | 16, 2, 4 in part | mesh-controller (builds are no longer asked), mesh-delivery (builder), mesh-sdk | 979 (the installed toolchains; closed with the toolchain images in R4g), 1304, 1305, the controller asking no build (opened when it is prepared) |
| R4f | A pull request carries one required status that says what it proved and how long each part took; the controller's merge gate and its replays are deleted with the two old statuses; the controller's deletions R3e makes possible go; each level's times are measured, and a bound fails a repository from the first check that meets it (ADR 0447, ADR 0450 §8, ADR 0453 clauses 4, 9, 10) | 21, 22, 23, 24 (the status; the scripts' absence is proved in R4g) | mesh-delivery (checker, assigner), mesh-controller (merge gate, replays, `delivery-plan`, `forget-checks`, the asker `mesh-delivery` deleted), mesh-catalog (gitea) | 1299 (the merge gate deleted; closes issue 709, the replays' socket), 1300, 1301, 1302 (one status), 1318 |
| R4g | The clean-up: the scripts of the repositories a module is built from, build-agent with its socket and Dockerfile code, the toolchain images and the word merge gate go; the builder's own module is delivered from the builder's keys (ADR 0453 clauses 5, 8, 10) | 4 (no container image builds remain), 24 (no `merge-check.sh` in a repository a module is built from) | mesh-controller (build-agent removed), mesh-catalog, mesh-delivery, mesh-tools (scripts deleted), hq | 1295 (`merge-check.sh` goes; the tests of 1293 and 1294 are R4c to R4e's), 979 (closed with the toolchain images), 1303 (the word merge gate struck); 1296 (how hq is checked) is the operator's and undated by this release |
| R5 | The assigner writes what each node should run by its six rules, compared with what the controller sends, and the view shows every difference; the operator answers the asks that need no proof of who answered in the view (ADR 0437 §10) | 17 | mesh-delivery (assigner), mesh-controller (grants keys) | opened when it is prepared |
| R6 | Each node applies from its wants, a new version reaches the module's canary first and the rest once it is healthy there, and the plans are deleted from the code | 18, 19 | the node-engine (`mesh-host`: the node agent), mesh-controller (plans deleted), mesh-delivery (the gate's delivery states go) | opened when it is prepared |
The issues are linked `depends-on` in this order; 976 depends on 975, and 979 also on 971. R3's prepare step on 2026-10-19 found ADR 0429 §20 contradicting itself and four release orders; ADR 0437 (accepted, the operator's Approve) cuts release 3 into R3a to R3d, each one release, and moves answering asks in the view to R5. R2c's prepare step on 2026-10-19 found one more order: the alive message is the only source of when the controller last heard a machine, which `nodes`, `node` and `status` read, so the controller takes that from presence (1183, R2c) and is delivered before the node-engine stops sending (1158, R2d); 1158 depends on 1183. In release 1-2, 1132 and 1136 depend on 1131, 1134 and 1135 on 1136, 1134 also on 1132 and 1128, and 1137 on 1134. Release 1-2 was confirmed as one milestone; its prepare step on 2026-10-18 found the release orders hidden in it, and the operator chose at 18:52 to cut it by them, so it is the milestones R1a to R2d above, each one release (the operator, 2026-10-19: a milestone is one release): an SDK change still ships before its users until release 4 (ADR 0429 §10); a new key on the wire goes only to a node that says it reads it; a new repository's first module registers at its merge's Approve (ADR 0432, issue 1146); the bus gives the watchman connect events only through a system account (ADR 0431); and release 2's other gaps are settled by ADR 0433 and ADR 0434. A node that is down has no effect on a delivery and plays what it missed when it returns (ADR 0435, the operator, 2026-10-18; issues 1160, 1161). Milestone 3's first live group (milestone 4's, on 2026-10-17) held one Approve but merged in two passes and went out in two delivery plans: a member's tests read the other member's repository, so the first merge made the second's verdict out of date, and a closed group never asked its composed verdict again (issues 1049, 1082). The operator put a fix of milestone 3 after milestone 4 and before milestone 5 on 2026-10-18; acceptance test 1 is proved again by it. The operator put milestone 5b after milestone 5 and before milestone 6 on 2026-10-18, as an issue rather than a research effort; where the planning goes is settled by a decision record before it is built (issue 1117), and milestone 6's build changes are read against it. Milestone 2b was
The issues are linked `depends-on` in this order; 976 depends on 975, and 979 also on 971. R3's prepare step on 2026-10-19 found ADR 0429 §20 contradicting itself and four release orders; ADR 0437 (accepted, the operator's Approve) cuts release 3 into R3a to R3d, each one release, and moves answering asks in the view to R5. R2c's prepare step on 2026-10-19 found one more order: the alive message is the only source of when the controller last heard a machine, which `nodes`, `node` and `status` read, so the controller takes that from presence (1183, R2c) and is delivered before the node-engine stops sending (1158, R2d); 1158 depends on 1183. In release 1-2, 1132 and 1136 depend on 1131, 1134 and 1135 on 1136, 1134 also on 1132 and 1128, and 1137 on 1134. Release 1-2 was confirmed as one milestone; its prepare step on 2026-10-18 found the release orders hidden in it, and the operator chose at 18:52 to cut it by them, so it is the milestones R1a to R2d above, each one release (the operator, 2026-10-19: a milestone is one release): an SDK change still ships before its users until R4e (ADR 0429 §10, ADR 0453 clause 2); a new key on the wire goes only to a node that says it reads it; a new repository's first module registers at its merge's Approve (ADR 0432, issue 1146); the bus gives the watchman connect events only through a system account (ADR 0431); and release 2's other gaps are settled by ADR 0433 and ADR 0434. A node that is down has no effect on a delivery and plays what it missed when it returns (ADR 0435, the operator, 2026-10-18; issues 1160, 1161). Milestone 3's first live group (milestone 4's, on 2026-10-17) held one Approve but merged in two passes and went out in two delivery plans: a member's tests read the other member's repository, so the first merge made the second's verdict out of date, and a closed group never asked its composed verdict again (issues 1049, 1082). The operator put a fix of milestone 3 after milestone 4 and before milestone 5 on 2026-10-18; acceptance test 1 is proved again by it. The operator put milestone 5b after milestone 5 and before milestone 6 on 2026-10-18, as an issue rather than a research effort; where the planning goes is settled by a decision record before it is built (issue 1117), and milestone 6's build changes are read against it. Milestone 2b was
put before milestone 3 by the operator on 2026-10-17. Milestone 3 builds ADR 0418, ADR 0419 and ADR 0421
(issues 977, 978 and 1022), and splits mesh-controller's unit tests from its integration tests (issue 895, the
operator on 2026-10-17), so the unit level holds its bound of ADR 0391; the integration level's sixty seconds
is issue 1055. Acceptance test 6 spoke of a ready mark when milestone 2 was delivered; restated for
the Approve, it is proved again by milestone 3.
R4's prepare step on 2026-10-20 cut release 4 by its orders into R4a to R4g, each one release (ADR 0453, the operator's Approve): the SDK ships before its users, the new parts run dark before anything relies on them, the status switch follows a measured run, and the scripts and build-agent go last;
R4d, the four image reworks, has no order of its own and may be delivered at any point between R4a and R4e. Where a record dates a rule by release 4, it reads the R4 release that builds the rule (ADR 0453 clause 15).
## What is not decided here
- A web view of deliveries beyond the pull request's comment.
+12 -3
View File
@@ -2,21 +2,23 @@
layer: to-be
status: in-progress
code: [hq 00-META/checks/words.py, mesh-catalog merge-check.sh]
updated: 2026-10-18
updated: 2026-10-20
decisions:
- "ADR 0429"
- "ADR 0244"
- "ADR 0006"
- "ADR 0008"
- "ADR 0302"
- "ADR 0354"
- "ADR 0458"
---
# 49 — The mesh in domains
**Every concept of the mesh belongs to one of eleven domains, which owns its one word. The glossary is
**Every concept of the mesh belongs to one of thirteen domains, which owns its one word. The glossary is
organised by them and is the authority; a word it retires is named in the entry that replaced it, with
where it is retired; and two checks hold the documents and the tools' descriptions to it.**
(ADR 0244; the eleventh, Media, ADR 0302.)
(ADR 0244; the eleventh, Media, ADR 0302; the twelfth, Config, ADR 0354; the thirteenth, Mail, ADR 0458.)
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. The glossary
@@ -38,6 +40,8 @@ shape — what each is for, where its records live, and how they depend on one a
| **Connectivity** | to make every node and module reachable by name where it should be, and unreachable where it should not | the controller |
| **Operator and conversation** | to let the mesh and its operator tell, ask and answer, and act only on an answer it can trust | the router, and the controller for authorising asks |
| **Media** | to find, fetch and file copies of the works a person watches, listens to and reads, each in the library its kind names | the `media-importer` module's settings and memory, and the servarr apps' own libraries |
| **Config** | to hold, check and resolve every value a module reads that someone may change, and say where each resolved value came from | the `mesh-config` module, in its buckets on the bus, with its store as the history behind them |
| **Mail** | to hold the operator's mail accounts, say each mail on the bus, note each mail for the operator and tell them of the mail that matters, and send a mail only on the operator's Approve | `mesh-mail` (its index database, attachment bucket and account buckets) and `mesh-mail-reader` (its database of mail entries, mail groups and reading lessons) |
Beside them, **the record** is this repository's own vocabulary — research, decisions, designs,
issues, playbooks, checks — listed because its words meet the mesh's.
@@ -71,6 +75,11 @@ a change of meaning upstream ripples down, never up. Arrows point downstream.
Media stands beside the diagram, below Module alone: its words are about what a person watches, not
about the mesh's machinery, and no other domain reads them.
Mail stands below the diagram's foot: it is downstream of Identity and access (the operator grants each mail
account to a caller) and of Operator and conversation (a mail send is an ask, a mail signal is a notice through
the operator channel), and upstream of nothing. It is the operator's own correspondence, which the mesh holds and
reads for them, not the mesh talking about itself, so it is not part of Operator and conversation (ADR 0458).
Delivery sits low because it reads health and data; in time it runs first, and asks Placement
to send. An authorising answer acts in whichever domain asked, through that domain's own verb: the
conversation owns the asking, never the act.
@@ -0,0 +1,305 @@
---
layer: to-be
status: in-progress
code: [mesh-catalog]
updated: 2026-10-20
decisions:
- "ADR 0440"
- "ADR 0378"
- "ADR 0379"
- "ADR 0359"
- "ADR 0361"
- "ADR 0138"
- "ADR 0167"
- "ADR 0439"
- "ADR 0201"
- "ADR 0436"
- "ADR 0458"
- "ADR 0459"
- "ADR 0460"
- "ADR 0461"
- "ADR 0462"
- "ADR 0278"
---
# 59 — The mail web client
**The operator's mail, every account of it, in a browser on the mesh's own machines. A module of its own,
`mesh-mail-web`, holds the seat `mail-webclient`; it renders pages on the server in Go and reaches mail only through
`mail-store`'s verbs, under a caller the operator grants account by account. Its route reaches the private network
only, under its internal name, and the operator signs in with Keycloak. A mail written there goes out only on the
operator's Approve, as every mail the mesh sends does.**
(ADR 0440, building on ADR 0378 §5 and ADR 0379 §2.)
**Then the model reads the mail for the operator.** A module of its own, `mesh-mail-reader`, holds the seat
`mail-reading`: it reads each new mail in batches, keeps a mail entry for it (its mail group, one line, its mark),
tells the operator on Telegram of the mail that matters in a mail signal, shows a scam warning on the page only, and
learns from the operator's corrections as reading lessons; it never changes the mailbox and keeps no mail text.
Reading and asking to send are granted apart first, and the pop-up for every new mail retires once mail signals
work. Later, once the mesh's threads exist, the operator asks questions about their mail in a mail conversation, a
thread with a mail context. Betterbird is removed once mail reading works on the laptop and the workstation.
(Research 066; ADR 0458 to 0462. The words are the glossary's Mail domain.)
**Acceptance.** In a browser on the laptop and on the workstation, the operator signs in, browses, searches and reads the mail of
every account `mesh-mail` holds, opens an attachment, and writes a mail that goes out on their Approve on Telegram;
the same page is not reachable from a machine outside the private network. A new mail that matters reaches the
operator's phone as one mail signal and stands at the top of the mail page in its mail group; a mail that does not
matter gives no pop-up and no mail signal anywhere.
## 1. The parts
```
browser (laptop, workstation) ── https://mail.<node>.internal ──▶ route proxy (internal reach only, the mesh's own certificate)
│
▼
mesh-mail-web (seat mail-webclient, once for the mesh)
│ sign-in: oidc-client ──▶ Keycloak
│ pages rendered on the server, a mail shown as text
▼ over the bus, as caller <machine>.mesh-mail-web
mail-store (mesh-mail): mail_accounts · mail_search · mail_read
mail_send ──▶ the operator's Approve (Telegram)
mail_send_status
mesh-mail's account bucket (ADR 0439 §2): folders and their counts
```
- **`mesh-mail-web`** is built from `mesh-mail`'s source, as `mesh-mail-notifier` is, and is assigned to the machine
that runs `mesh-mail` (the control-node today). It keeps no mail: no database, nothing on disk (ADR 0440 §1, §4).
- **`mesh-mail`** gains its account bucket (ADR 0439 §2, issue 1255) and a caller check on `mail_send_status`
(ADR 0440 §7, issue 1261), both released with `mesh-mail-web`. It
answers `mesh-mail-web` only for the accounts its trusted setting `grants` gives `mesh-mail-web` (ADR 0359 §4,
ADR 0378 §13). `mail_send_status` becomes part of `mail-store`'s protocol (ADR 0440 §7).
- **The browser never reaches the bus.** The bus has no WebSocket listener or sign-in for a person yet (research 036,
gap G1), so every page is HTML from `mesh-mail-web` (ADR 0440 §5).
## 2. Reachable from the private network only (ADR 0440 §2)
The operator's rule of 2026-10-15: a web UI for mail is reached only over the mesh's private network, never from
outside. Two things hold it:
1. **Its route's reach is `internal`** (ADR 0138 §7). Reach is a setting of the machine a module runs on, keyed by
port, and with none the route proxy composes a public name as well as the internal one. So `mesh-mail-web`'s
`reach` is set to `internal` on its machine before it is assigned there. The route proxy then composes only
`mail.<node>.internal`, serves it only to the mesh's own machines (ADR 0167 §6), and certifies it with the mesh's
own authority.
2. **The module answers only its internal name.** A request whose host is not `mail.<node>.internal` is answered
404, so a public name composed by mistake shows nothing.
Measured on 2026-10-19: the laptop and the workstation hold the mesh's authority (`mesh-internal-ca.crt`) as a trust anchor, and
Firefox on both reads the system's trust through p11-kit, so the page opens there without a warning. Away from the
private network (the laptop travelling without its tunnel) the page is unreachable; Betterbird stays the offline client
until it is removed (milestone 3, §11).
## 3. Signing in (ADR 0440 §3)
`mesh-mail-web` requires `oidc-client` from Keycloak, as grafana does, and uses the authorisation-code flow. It answers
no page, attachment or send to a browser that has not signed in, and lets in only the identities its trusted setting
`operators` names (nobody by default). The session cookie is `Secure`, `HttpOnly` and `SameSite=Strict` and ends
after 12 hours. Every form that changes something (today only the send) carries a token bound to the session.
The operator chose this on 2026-10-19: without it, any process on any mesh machine, agents included, could read every
account through the page and get around `grants`.
## 4. The pages
| page | what it shows | how |
|---|---|---|
| accounts | every granted account, its state, its folders and their counts | `mail_accounts`; the folders from `mesh-mail`'s account bucket, only of an account `mail_accounts` answered (the bucket's read is wider than the grant, ADR 0440 §4) |
| folder | one folder's mails, newest first, in pages of 50 | `mail_search` with the folder; the next page asks for mails before the last one shown |
| search | mails across every granted account by words, sender, recipient, date, attachment | `mail_search`'s filters (issues 1228-1232) |
| mail | one mail: its headers, its body as escaped text, its attachments | `mail_read` |
| attachment | one attachment, as a download | `mail_read` with the attachment's key |
| write | from (a granted account), to, subject, body; after sending, the Approve's state | `mail_send`, then `mail_send_status` |
- **A mail is shown as text** (ADR 0440 §5): `mail_read` turns HTML into text, the page escapes it, and nothing
remote is loaded. An attachment answers `Content-Disposition: attachment` and `X-Content-Type-Options: nosniff`.
Every page carries a content security policy that allows no script or style from elsewhere.
- **A send is an ask** (ADR 0361 §2): the page says the ask is raised, then follows `mail_send_status` (ADR 0436
§12's states, now part of `mail-store`'s protocol, ADR 0440 §7) until a final one: sent, failed, refused, not
proven, declined, expired or unknown. The operator's Approve comes on Telegram, as for every mail the mesh sends.
- **The seat's verbs** (ADR 0378 §5): `open` answers the page's address, and with an account and a mail that mail's
page; `status` answers whether it serves and which accounts it is granted.
## 5. Reading and asking to send, granted apart (ADR 0460)
`mesh-mail`'s trusted setting `grants` gives a caller, per account, **reading** (that account's `MAIL` events,
`mail_accounts`, `mail_search`, `mail_read`) and **asking to send** (`mail_send`, `mail_send_status`), each on its own,
set as `grants` is today. A caller granted reading only is refused by both send tools, with the missing grant named;
one granted asking to send only is refused every read. When the split is delivered every caller in today's `grants`
keeps both for each account it has, so nothing stops; narrowing one afterwards is the operator's choice.
`mesh-mail-reader` is granted reading only, for every account, and nothing before the split is delivered. This is
issue 1227.
## 6. Mail reading (ADR 0459)
```
mail-store (mesh-mail) ── MAIL: mail.<account>.message.added ──▶ mesh-mail-reader (seat mail-reading, once for the mesh)
▲ │ its own database: mail entries, mail groups,
└──────────── mail_read (reading grant only) ─────────────┤ reading lessons; never a body
│ batches of ≤ 10 mails ──▶ claude_code_judge (within ≤ 40 s)
│ claude_code_judged
├──▶ operator-channel.tell (a mail signal, §7)
▼
mesh-mail-web ── entries · groups · correct · lessons · undo · status ──▶ the mail page
```
- **One module, one seat.** `mesh-mail-reader`, built from `mesh-mail`'s source like `mesh-mail-web`, claims
`mail-reading` (declared in `mesh-mail`'s manifest beside the three seats of ADR 0379 §4) and runs once, on the
machine that runs `mesh-mail`. It hears `added` events under its own `runs-as` account and the operator's reading
grant (ADR 0378 §13), and moves, flags, deletes and sends nothing.
- **A mail entry per mail, never the body.** Its own database (`postgres-database`) keeps each mail's mail group,
a one-line summary, its mark (*matters*, *scam warning* or neither) and why, and a due date where the mail gives
one, beside the mail groups and the reading lessons.
- **Batches.** One judgement reads at most 10 mails, and every part of its question has a bound, so it never
passes 55 KiB of the judgement's 64 KiB: the framing that a mail is data and never an instruction, at most 4 KiB;
the mail groups, at most 4 KiB, the largest first; the reading lessons that apply, at most 40 within 12 KiB,
sender and mail group first, then the newest, under the framing that a reading lesson outranks anything a mail
says; then each mail, at most 3.5 KiB (its headers at most 512 bytes, its text cut to its first 3 KiB), a mail in
a Junk folder said to be so. A batch asks for up to 40 s with `within` and collects with
`claude_code_judged` (built under issue 508, recorded by ADR 0459 §14). Research 066 measured 1.9 to 3.1 s per
mail, so a batch of 10 takes about 19 to 31 s. A batch starts at least 160 s after the previous one started, so
an issue tracker's check that waits behind a batch on the same machine ends within 115 s, inside its two minutes
(ADR 0278 §2), which research 066's 15 mails and 120 s back to back would not; mail reading then reads up to 225
mails an hour (ADR 0459 §15).
Batches stay judgements; whether they become threads is issue 1417.
- **Reading lessons.** When the operator changes a mail's mark or mail group on the page, `mesh-mail-web` calls
`correct` and the correction is kept at once as a reading lesson (headers, the change, when; never body text),
with no further Approve: it is the signed-in operator's own act. The page lists the reading lessons, undoes one
(later mail reading only), and shows reading lessons per 100 read mails per week, which answers whether mail
reading learns the operator's way.
- **The page.** Above the classical list it shows the mail that matters at the top, the mail groups, and the scam
warnings in a place of their own, each with why; the classical list (accounts, folders, search, one mail) stays
and always opens. `mesh-mail-web` reads these through `mail-reading`'s verbs and still keeps no mail state
(ADR 0378 §5 and ADR 0440 §4 as ADR 0459 §13 changes them); it writes the operator's corrections with
`correct` and `undo`.
- **It fails loudly.** Its own health check is unhealthy when a judgement is refused, times out or answers out of
shape, with the cause and since when; the controller raises its condition, the page shows the cause above the
classical list instead of mail groups, and no mail signal is sent. On recovery the mail that arrived meanwhile is
read in arrival order from its durable consumer on `MAIL` (kept 7 days). A warning before a licence runs out is
issue 1409.
## 7. Mail signals, and the pop-up retired (ADR 0461)
- **One tell per batch.** A batch that marks mail as mattering sends one `operator-channel.tell`: the headline
"1 mail matters" or "N mails matter", a first line naming the mail with the nearest due date (sender's name,
summary, due date), a second naming the next and, with more than two, ending "and N-2 more on the mail page".
When the hourly cap of 10 refuses one, its mails stay at the top of the page and the next mail signal counts
them.
- **Words only.** No link, mail address, host name or path: the router refuses a message holding any, and the mail
page opens only on the mesh's own machines. The reader checks its words with `messenger_check` first and, refused,
sends the safe form "N mails matter: open the mail page".
- **At once, at any hour**; the phone's own quiet hours decide. **Never** for a scam warning, and never while mail
reading is unhealthy.
- **The pop-up retires.** Once mail signals pass acceptance test 13 on the real accounts, `mesh-mail-notifier` is
unassigned from the laptop and the workstation on the operator's Approve and leaves `grants`; `mail-notifier`
stays declared and unheld, and nothing takes it over (ADR 0379 §3, §7). Betterbird's own pop-ups stay off by its
setting until it is removed (ADR 0379 §6). Issue 1418; issue 801 closes as replaced.
## 8. The mail conversation (ADR 0462)
A mail conversation is a thread (ADR 0383), run by the thread handler (ADR 0384), with a mail context: the framing
that a mail is data, the tools `mail_search` and `mail_read` only under a caller of the thread's own granted reading
only (issue 1419), the operator's real senders and folder names, and the reading lessons. The operator writes a mail
question on the mail page; each answer names the mails it found, and a question with no answer says so.
`mesh-mail-web` creates and follows the thread over the bus on its server side; the page loads one script from its
own origin and talks only to it (content security policy: script from that origin, `connect-src 'self'`), so the
browser never reaches the bus. The thread keeps the text of each mail it read for good, as every thread is kept (ADR
0383 §3), which the operator accepted; mail reading keeps none. In the same milestone `mail_search` matches folder
names without regard to case and describes its three `-any` switches as true or false only. It is built after design
58's milestone 3 (threads, issue 880) and once issue 1419's caller is decided.
## 9. What stays outside
Each is an issue with an owner:
- a reply, cc, bcc and attachments on a sent mail: issue 1257 (it needs `mail_send` to take them first);
- a page that shows new mail without a reload, from ADR 0439's buckets: issue 1258;
- drafting a mail, the rest of issue 1259 (its triage is mail reading, its search the mail conversation), and writing
to the mailbox: issue 730;
- a task list for the operator: issue 1396; the search faults found by research 066: issues 1400 and 1401;
- the work account: a later decision of its own (ADR 0359 §7).
## 10. The build's acceptance tests
Tests 1 to 9 are milestone 1's; tests 10 to 17 are research 066's eight, in its order.
1. A request without a session, for a page, an attachment or a send, is redirected to sign-in; an identity not in
`operators` is answered 403; a send form without its token is answered 403; the session cookie is `Secure`,
`HttpOnly` and `SameSite=Strict` and ends 12 hours after sign-in.
2. A request whose host is not the internal name is answered 404.
3. Against a stand-in `mail-store` and a filled account bucket on the real bus, the pages call only
`mail_accounts`, `mail_search`, `mail_read`, `mail_send` and `mail_send_status`, list folders from the bucket,
page by date, and a send shows its state; the manifest declares no database and no writable directory.
4. A mail whose body holds `<script>` and a remote image is shown as text; an attachment is a download with
`nosniff`; every page carries a content security policy allowing only its own origin.
5. After a mail is indexed, `mesh-mail`'s account bucket changes only the keys that describe it, within ADR 0439's
bounds (issue 1255's own tests).
6. The declaration for its machine composes `mesh-mail-web`'s route with its internal name only (`mesh-controller.plan`).
7. `mail_send_status` answers each state of ADR 0436 §12, and `unknown` for an id it does not know and for another
caller's send (a unit test of `mesh-mail`, issue 1261).
8. The seat's verbs: `open` answers the page's address, and with an account and a mail that mail's page; `status`
answers whether it serves and the accounts it is granted (a unit test of `mesh-mail-web`, issue 1262).
9. The acceptance sentence at the top, live: in a browser on the laptop and on the workstation, for every account.
10. A new mail on each account has a mail entry, in a mail group, within 5 minutes of its mail event; a test searches
`mesh-mail-reader`'s database for a known body line and finds none.
11. The reader's caller is refused by `mail_send` for every account, and no mail is moved, flagged or deleted by it.
12. (Milestone 4.) On the mail page, a mail question by a person, by a time range and by "what needs a reply" finds
its target mails, research 066's three failed kinds included; a question with no answer says so; the conversation
is a thread readable through the thread seat; and the browser makes no request but to the mail page's own origin.
13. A batch with one mail that matters sends exactly one `tell` naming it, with no URL, mail address or host name in
its words, and the mail is at the top of the page; a sender whose name carries a host name still gives a
mail signal, in the safe form.
14. Research 066's 20 steering mails, read again, mark no injected words as mattering, and every phishing decoy that
is marked is a scam warning, which sends no `tell`.
15. A reading lesson given on a real mail changes how the next mail of that sender is noted; undone, it no longer
does; the page lists it.
16. With the judgement refused (the reader's licence binding removed in the integration test), the condition is
raised with its cause, the page shows it over the classical list, no `tell` is sent, and after recovery the mail
that arrived meanwhile is read in arrival order.
17. With `mesh-mail-notifier` unassigned, a new mail that does not matter shows nothing on the laptop or the
workstation, and one that matters gives one mail signal.
Tests 1 to 4 are one integration test that starts the module whole against the real bus (playbook 07 step 3 item 4);
tests 5 and 7 are `mesh-mail`'s own and test 8 `mesh-mail-web`'s, unit tests; test 6 is read before the module is
assigned; test 9 is proven live after the delivery.
Tests 11, 13 (with a stand-in router), 16 and the grant split's refusals are one integration test that starts
`mesh-mail` and `mesh-mail-reader` whole against the real bus and store; the batch's budget, the seat's verbs and the
mail signal's words are unit tests; tests 10, 13, 14, 15 and 17 are proven live after milestone 2's delivery, test 13
before `mesh-mail-notifier` is unassigned; test 12 with milestone 4.
## 11. Milestones
| # | Outcome, in one sentence | Proves | Repositories and their parts | Depends on |
|---|---|---|---|---|
| 1 | In a browser on the laptop and the workstation, the operator signs in and browses, searches and reads every account's mail with its attachments, and writes a mail that goes out on their Approve | tests 1 to 9 | mesh-catalog: `mesh-mail`'s account bucket (issue 1255, ADR 0439 §2); the module `mesh-mail-web` (manifest, sign-in, the pages, the send, the seat's verbs, the integration test) | — |
| 2 | Each new mail is read into a mail group on the mail page; the mail that matters reaches the operator's phone as one mail signal, a scam warning shows on the page only, the operator's corrections become reading lessons, and the pop-up for every new mail is gone | tests 10, 11, 13 to 17 | mesh-catalog: `mesh-mail`'s grants split (issue 1227) and the seat `mail-reading` declared; the module `mesh-mail-reader` (batches, mail entries, reading lessons, mail signals, health, the integration test); `mesh-mail-web`'s mail groups, mail signals, scam warnings and reading lessons; `mesh-mail-notifier` unassigned (issue 1418) | 1 |
| 3 | Betterbird is gone from the laptop and the workstation, its profile and mail kept in the home | ADR 0379 §2 | mesh-catalog: Betterbird unassigned on the operator's Approve, then the `betterbird` module removed from the catalogue (issue 806) | 2 |
| 4 | On the mail page the operator asks questions about their mail and gets the mails that answer them, in a mail conversation kept as a thread | test 12 | mesh-catalog: `mesh-mail-web`'s mail conversation as a client of the thread seat; the thread's mail tools under a caller of their own granted reading only (issue 1419); `mesh-mail`'s folder case and `-any` switches | 2 (the grant split and the reading lessons); design 58's milestone 3 (issue 880) |
**Milestone 1, confirmed by the operator on 2026-10-19, is delivered;** its live proof on the laptop and the
workstation (a search, an attachment, one send on the Approve) is still the operator's. Its issues, one repository
part each, in their order: 1255 (`mesh-mail`'s account bucket), 1261 (`mail_send_status` answers only its caller),
1262 (the module), 1263 (sign-in), 1264 (accounts, folders and mails), 1265 (search), 1266 (a mail and its
attachments), 1267 (writing a mail), 1268 (the integration test). After its delivery four values were set, each on
the operator's Approve: `mesh-mail-web`'s `reach` on its machine (`internal`), its assignment there, its
`operators`, and its caller in `mesh-mail`'s `grants` for every account.
**Milestones 2 to 4, confirmed by the operator on 2026-10-20.** Their issues, one repository part each, in their
order (each depends on the one before unless said):
- Milestone 2: 1227 (`mesh-mail`'s grants split), 1426 (the seat `mail-reading` and the module `mesh-mail-reader`),
1427 (batches, mail entries and mail groups), 1428 (reading lessons and the seat's verbs; with 1427 after 1426),
1429 (mail signals; after 1427), 1430 (the mail page; after 1428 and 1429), 1431 (the integration test; after
1430 and 1227), 1418 (`mesh-mail-notifier` unassigned; after 1431 and test 13 live).
- Milestone 3: 806 (Betterbird removed; after 1418).
- Milestone 4: 1433 (`mail_search`'s folder case and `-any` switches; depends on none), 1419 (the thread's
reading-only caller; a decision first, after 1227 and design 58's milestone 3, issue 880), 1432 (the mail conversation;
after 1431, 1419 and issue 880).
Milestone 2 is one release of `mesh-catalog`: the grant split, the new seat, the new
module and the page change ship together, and `mesh-mail-reader` is granted reading only after that delivery, on the
operator's Approve; `mesh-mail-notifier` is unassigned only after test 13 passes live. Milestone 3 is issue 806.
ADR 0379 §2's bar for removing Betterbird is milestone 1's; keeping Betterbird until mail reading (milestone 2) works,
and not until the mail conversation, is the lead's proposal, for the operator to confirm with the milestones
(playbook 04, step 5). Milestone 4 waits for threads and for the decision that names its thread's reading-only caller (issue 1419, which needs a decision first).
+1
View File
@@ -58,6 +58,7 @@ document is written and this one's status becomes `implemented`.
| [`56-the-mail-client-set-up-by-the-mesh.md`](56-the-mail-client-set-up-by-the-mesh.md) | **Designed.** On every node with a display server, the `betterbird` module installs Betterbird from a pinned upstream release mirrored into the artifact store, ships the Owl add-on pinned and installed by Betterbird's enterprise policy, and declares the operator's mail accounts in a trusted setting without credentials; each account signs in once at a person's first use, and one the profile already holds is adopted by its address | ADR 0316, ADR 0317 |
| [`57-building-config-and-provisioning.md`](57-building-config-and-provisioning.md) | **Designed.** The work of ADR 0348 and ADR 0349 as 37 packages, each an issue with its proof: foundations, template files, config in its own module, the provisioner and its client, secrets in the vault, bindings from providers, collections, a module's runtime readers, and removing the old forms; at most five worked at once | ADR 0348, ADR 0349 |
| [`58-agent-work-as-the-meshs-own.md`](58-agent-work-as-the-meshs-own.md) | **Designed.** Agent work as the mesh's own: a thread seat keeps every thread whole on the bus; a thread handler runs the harness headless in a sandbox per session, the workspace on the thread's own forge branch; a workspace toolkit and named tools, no general command runner; one identity seat for persons, agents and modules, roles of grants, every tool an effect, signed bus logins, and the switch to them one identity at a time | ADR 0383, ADR 0384, ADR 0385, ADR 0386, ADR 0387, ADR 0388, ADR 0389, ADR 0390 |
| [`59-the-mail-web-client.md`](59-the-mail-web-client.md) | **In progress.** The operator's mail, every account, in a browser on the mesh's own machines: a module of its own, `mesh-mail-web`, holds `mail-webclient`, renders pages on the server and reads and sends only through `mail-store`'s verbs under a caller granted per account; its route reaches the private network only, the operator signs in with Keycloak, and a mail goes out only on the operator's Approve; then Betterbird is removed | ADR 0440, ADR 0378, ADR 0379 |
## Not yet written
+4 -1
View File
@@ -55,7 +55,10 @@ in running prose; a quotation keeps its words.
## Ground rules
- **Markdown only** — but for the checks in `00-META/checks/` and `merge-check.sh` at the root, which
runs them on every pull request as its `mesh/repo-check` (ADR 0238).
runs them on every pull request as its `mesh/repo-check` (ADR 0238), and
`00-META/process/playbooks.html`, a drawn overview of the playbooks kept for a later docs site (the operator,
2026-10-19); a change to a playbook's steps updates it in the same pull request. Nothing checks that
yet: issue 1222 asks for the check.
No new top-level folders without explicit confirmation.
- **Status lives in YAML frontmatter** — on design docs (`layer`, `status`, `code`, `updated`,
and `decisions` as "ADR NNNN"). A decision record's and a research effort's status live in the knowledge
+1 -1
View File
@@ -40,7 +40,7 @@ Implementation lives in the code repositories — see
## Rules
- Markdown only.
- Markdown only, but for the checks and one drawn overview of the playbooks (`00-META/process/playbooks.html`).
- No new top-level folders without explicit confirmation.
- Knowledge flows `GENESIS → RESEARCH → DESIGN`. Research graduates into design only after
analysis against GENESIS confirms alignment, and only through a recorded decision.