221 Commits
Author SHA1 Message Date
mesh-tools e65f897a1f Merge pull request 'Playbooks 07 and 08 name the checker's try beside the controller's (issue 774, ADR 0561), rebased onto L2' (#495) from feat/774-vault-mints-l2 into main 2026-10-29 07:44:15 +00:00
jschoubben 7fd89de7cb Playbook 07: item 9 names no wrong item
mesh/delivery merged: source.main moved to novox/hq@e65f897a1fda
mesh/delivery-group group feat/774-vault-mints-l2 merged: every member is merged
mesh/check pass 7fd89de7cb81 on e65f897a1fda | unit 39 run, 0 kept, 1 skipped | integration 39 run, 0 kept, 1 skipped | composition passed
2026-10-29 01:56:19 +01:00
jschoubben f4fdbde558 Playbook 07: rules for a delivery group's mesh-sdk pins, learned on 2026-10-28, so the next group does not meet the same refusals 2026-10-29 01:56:19 +01:00
jschoubben 7d022710b3 Playbooks 07 and 08: name the controller's try too while its verb exists (issue 774)
mesh/delivery-group group feat/774-vault-mints-l2 ready: every member ready, and composed together they pass
mesh/delivery closed: a newer head of the same pull request
mesh/check pass 7d022710b34b on eec73b8af0d7 | unit 1929 run, 4791 kept, 47 skipped | integration 1993 run, 4856 kept, 49 skipped | over bound: unit, …
2026-10-28 20:37:05 +01:00
jschoubben f89b8599c6 Playbooks 07 and 08 name the try by the checker's seat: mesh-delivery-checker.try replaces mesh-controller.try (ADR 0547 §10, issue 774) 2026-10-28 20:36:59 +01:00
mesh-tools eec73b8af0 Merge pull request 'Design 47 and the glossary: placement is bus state of the seat mesh-placement (issue 1126, delivery 2 of 2)' (#494) from feat/1126-placement-seat into main 2026-10-28 18:32:58 +00:00
jschoubben 1aa887dd28 Glossary: placement, mesh-placement, placement proposal, candidate and hand-back replace the placement file
mesh/delivery merged: source.main moved to novox/hq@eec73b8af0d7
mesh/delivery-group group feat/1126-placement-seat merged: every member is merged
mesh/check pass 1aa887dd2898 on eec73b8af0d7 | unit 39 run, 0 kept, 1 skipped | integration 39 run, 0 kept, 1 skipped | composition passed
2026-10-28 18:25:31 +01:00
jschoubben aa4c2fe7ef Design 47: placement is bus state of the seat mesh-placement, changed by a proposal (ADR 0567) 2026-10-28 18:25:19 +01:00
mesh-tools 292232dad2 Merge pull request 'Playbooks 07 and 08: every review asks where each piece of state lives and whether a repository holds mesh setup' (#493) from pb/state-and-seams into main 2026-10-28 16:46:35 +00:00
jschoubben a9830f5110 Playbooks 07 and 08: the review asks where state lives and how seams travel, and whether a repository holds mesh setup
mesh/delivery merged: source.main moved to novox/hq@292232dad226
mesh/check pass a9830f51106c on eec73b8af0d7 | carried from 292232dad226 | unit 39 run, 0 kept, 1 skipped | integration 39 run, 0 kept, 1 skipped | co…
2026-10-28 17:43:44 +01:00
mesh-tools 02a18c9aa8 Merge pull request 'Design 47's L5 row and the glossary follow ADR 0565' (#492) from docs/0565-homing into main 2026-10-28 13:08:30 +00:00
jschoubben 606956da97 Design 47 L5 and the glossary: add clause 8 and the lease-write gate, both readers of kept, upgrade answered side by side
mesh/delivery merged: source.main moved to novox/hq@02a18c9aa8ab
mesh/check pass 606956da970a on eec73b8af0d7 | carried from 292232dad226 | unit 39 run, 0 kept, 1 skipped | integration 39 run, 0 kept, 1 skipped | co…
2026-10-28 13:29:42 +01:00
jschoubben d088f17cd2 Design 47 L5 and the glossary follow ADR 0565: say which parts leave the controller and which seat takes each 2026-10-28 13:28:11 +01:00
mesh-tools dd03229ea8 Merge pull request 'Playbook 07 follows report-cadence: the short report every five minutes, the full one every thirty' (#491) from pb/report-cadence into main 2026-10-28 11:35:44 +00:00
jschoubben 781392b121 Playbook 07: the report follows the report-cadence section (short every 5 min, detailed every 30 min)
mesh/delivery merged: source.main moved to novox/hq@dd03229ea886
mesh/check pass 781392b12137 on eec73b8af0d7 | carried from 292232dad226 | unit 39 run, 0 kept, 1 skipped | integration 39 run, 0 kept, 1 skipped | co…
2026-10-28 12:32:05 +01:00
mesh-tools c2f4bb2d48 Merge pull request 'The glossary's word is catalog, not catalogue (group words/catalog)' (#490) from words/catalog into main 2026-10-28 11:31:29 +00:00
jschoubben ea0c62355c The glossary's word is catalog: replace the spelling catalogue in running prose
mesh/delivery merged: source.main moved to novox/hq@c2f4bb2d485d
mesh/delivery-group group words/catalog merged: every member is merged
mesh/check pass ea0c62355c1c on eec73b8af0d7 | carried from 292232dad226 | unit 39 run, 0 kept, 1 skipped | integration 39 run, 0 kept, 1 skipped | co…
2026-10-28 11:27:34 +01:00
mesh-tools 8f8393fa93 Merge pull request 'Playbooks 07 and 08: lessons of 2026-10-28 (the report's tree, its cadence, named local integration tests, member repositories during a check, delivered means what the nodes run, real-shaped fixtures)' (#489) from pb/lessons-2026-10-28 into main 2026-10-28 10:15:18 +00:00
jschoubben dd36b7557a Playbooks 07 and 08: the local integration run is the named tests that exercise the change; the report's tree is drawn from the verb
mesh/delivery merged: source.main moved to novox/hq@8f8393fa9343
mesh/check pass dd36b7557ae6 on 8f8393fa9343 | carried from 2ea3368b76d1 | unit 39 run, 0 kept, 1 skipped | integration 39 run, 0 kept, 1 skipped | co…
2026-10-28 11:12:23 +01:00
jschoubben d348256090 Playbooks 07 and 08: lessons of 2026-10-28 (progress tree, report cadence, local integration run, group members, delivered, fixtures) 2026-10-28 11:10:26 +01:00
mesh-tools 2ea3368b76 Merge pull request 'Design 47: ADR 0564 homes health (L4), and milestones L5 to L7 give the controller's other jobs their homes before L3's lease write' (#488) from docs/47-adr-0564 into main 2026-10-28 09:46:31 +00:00
jschoubben 3c2d5af436 Design 47: milestones L5 to L7 join L4 as the homes of the controller's other jobs
mesh/delivery merged: source.main moved to novox/hq@2ea3368b76d1
mesh/check pass 3c2d5af436fb on 8f8393fa9343 | carried from 2ea3368b76d1 | unit 39 run, 0 kept, 1 skipped | integration 39 run, 0 kept, 1 skipped | co…
Brings in docs/47-072-milestones (edbb53b): the rows L5 to L7 and the paragraph that orders L4 to L7
before L3's lease write. L4 keeps the row that follows ADR 0564; that branch's earlier L4 wording,
which named events the record does not keep, is dropped.
2026-10-28 10:42:35 +01:00
jschoubben bcad1e363a Design 47 and the glossary home ADR 0564: health leaves the controller in milestone L4
ADR 0564 is accepted on the operator's Approve. Design 47 names it and gains L4's row (issue 1952);
the glossary gains condition keeper, observer and take-over, and finding, self-check and the
watchman say where they are kept and held now.
2026-10-28 10:41:29 +01:00
jschoubben edbb53bb78 Design 47: milestones L4 to L7 give the controller's other jobs their homes before L3's lease write
The operator said yes on 2026-10-28 to making research 072's proposed homes milestones after L3,
so their decision records and design can start beside L3. ADR 0547 §11 makes the lease write wait
for every one of them; issues 1952 to 1955 carry each, a decision record first.
2026-10-28 09:40:05 +01:00
mesh-tools 32da464794 Merge pull request 'How-we-build: a bus permission is proven under the real permission (ADR 0560); playbook 07 notes the 2026-10-27 failure' (#486) from pb/seed-lessons-2026-10-27 into main 2026-10-27 21:19:59 +00:00
mesh-tools 10bc8d396c Merge pull request 'Design 47: R4d says what it became, three image reworks done and n8n left by ADR 0506' (#487) from design/47-r4d-done into main 2026-10-27 21:16:02 +00:00
jschoubben 8e732e22ae Design 47: R4d is done; record the three reworks, n8n's exit and the issues (ADR 0480, 0487, 0497, 0506)
mesh/delivery merged: source.main moved to novox/hq@10bc8d396ceb
mesh/check pass 8e732e22aeff on 8f8393fa9343 | carried from 2ea3368b76d1 | unit 39 run, 0 kept, 1 skipped | integration 39 run, 0 kept, 1 skipped | co…
2026-10-27 22:14:06 +01:00
jschoubben 6ef40bbcc7 How-we-build: a bus permission is proven under the real permission (ADR 0560); playbook 07 keeps only the failure note, as the operator asked
mesh/delivery merged: source.main moved to novox/hq@32da46479424
mesh/check pass 6ef40bbcc7d8 on 8f8393fa9343 | carried from 2ea3368b76d1 | unit 39 run, 0 kept, 1 skipped | integration 39 run, 0 kept, 1 skipped | co…
2026-10-27 20:36:54 +01:00
jschoubben 7ff4c7ce68 Playbook 07: the developer runs the node-permission test locally as a named exception to step 1
mesh/delivery closed: a newer head of the same pull request
mesh/check pass 7ff4c7ce6888 on 420751b214c0 | unit 39 run, 0 kept, 1 skipped | integration 39 run, 0 kept, 1 skipped | composition passed
2026-10-27 20:29:11 +01:00
jschoubben 0d1aa93e37 Playbook 07: keep only the node-agent permission lesson; the operator keeps merging before the check by choice (2026-10-27) 2026-10-27 20:28:16 +01:00
jschoubben 1afab9739e Playbook 07: a node-agent change runs under the node's real permissions before review approves, and a pull request whose safety is its check is approved only on its passing verdict (issue 1126, 2026-10-27) 2026-10-27 20:27:41 +01:00
mesh-tools 420751b214 Merge pull request 'Design 47: a placement change is answered by the nodes it places before it merges (ADR 0559, issue 1126)' (#485) from feat/1126-l2b into main 2026-10-27 07:21:07 +00:00
jschoubben 043f8ecf9c Design 47 and the glossary: a placement change is answered by the nodes it places before it merges (ADR 0559, issue 1126)
mesh/delivery merged: source.main moved to novox/hq@420751b214c0
mesh/delivery-group group feat/1126-l2b merged: every member is merged
mesh/check pass 043f8ecf9cfa on 420751b214c0 | unit 39 run, 0 kept, 1 skipped | integration 39 run, 0 kept, 1 skipped | composition passed
The seed and every later placement pull request were to be checked as equal to what runs,
which no composition outside the node can show; the nodes now answer a placement candidate.
2026-10-27 02:14:09 +01:00
mesh-tools 132aae8433 Merge pull request 'Design 47 says what a node composes from: placement, its config layers, and the node record (issue 1126)' (#482) from feat/1126-nodes-compose into main 2026-10-26 21:11:12 +00:00
jschoubben 28d84f31bf Merge remote-tracking branch 'origin/main' into feat/1126-nodes-compose
mesh/delivery merged: source.main moved to novox/hq@132aae843383
mesh/delivery-group group feat/1126-nodes-compose merged: every member is merged
mesh/check pass 28d84f31bfcb on 132aae843383 | unit 39 run, 0 kept, 1 skipped | integration 39 run, 0 kept, 1 skipped | composition passed in 2ms
2026-10-26 21:56:52 +01:00
mesh-tools 278ca42b3d Merge pull request 'Playbooks 07 and 08: one release, one check, no try, push once after the review (ADR 0548)' (#484) from pb/one-check-per-release into main 2026-10-26 19:55:30 +00:00
jschoubben 1dab64134a Playbooks 07 and 08: the one check is one status, mesh/check; drop the leftover try wording (review fixes)
mesh/delivery merged: source.main moved to novox/hq@278ca42b3df2
mesh/check pass 1dab64134a56 on 132aae843383 | unit 39 run, 0 kept, 1 skipped | integration 39 run, 0 kept, 1 skipped | composition passed
2026-10-26 20:52:36 +01:00
jschoubben a0e4c1fe17 Playbooks 07 and 08: one release, one check, no try, push once after the review (ADR 0548)
Build capacity is the mesh's scarcest resource: a try before and after the review plus a check per fix round on an open pull request spent it several times per release.
2026-10-26 20:50:37 +01:00
jschoubben 7c88f297fc Design 47: a settings-form module's values in the placement file, the vault's own, both sources' readers, the switch when placed, and the order of the delivery and the seed (issue 1126)
mesh/delivery-group group feat/1126-nodes-compose checking: 4 of 6 member(s) ready
mesh/delivery closed: a newer head of the same pull request
mesh/check checking 7c88f297fc59 on 278ca42b3df2 | novox/hq#482 in group feat/1126-nodes-compose
2026-10-26 19:45:32 +01:00
jschoubben 495d740d5f Design 47: a placed node's want says self and names no composition; a yes names the composition allowed (issue 1126, ADR 0547 §2, §3)
mesh/delivery closed: a newer head of the same pull request
mesh/check failed: ace: novox/mesh-catalog#505 does not merge into the trunk: files in conflict: .gitignore | 495d740d5ffc on d698942b1d56 | unit 1245…
2026-10-26 19:17:48 +01:00
jschoubben df4ff3d039 Design 47: a model-access consumer placed before the vault serves licences is refused by the guard (issue 1126) 2026-10-26 19:09:16 +01:00
mesh-tools d698942b1d Merge pull request 'Design 47's milestone table names each issue once, and design 53 says milestones are records (issue 1781)' (#483) from feat/1781-milestone-records into main 2026-10-26 18:06:44 +00:00
jschoubben a6cc3d2e0b Design 47: a node never applies a stand-in, and composes nothing of its own until the vault seals its secrets to it (issue 1126)
mesh/delivery closed: a newer head of the same pull request
mesh/check checking a6cc3d2e0b68 on 515c405c8939 | novox/hq#482 in group feat/1126-nodes-compose
2026-10-26 18:59:06 +01:00
jschoubben b6572acd72 Design 47: every read a node's account gains to compose, and that a module with an own secret does not move until the vault seals it to the node (issue 1126, ADR 0555)
mesh/delivery-group group feat/1126-nodes-compose checking: 4 of 5 member(s) ready
mesh/delivery closed: a newer head of the same pull request
mesh/check checking b6572acd72e5 on 515c405c8939 | novox/hq#482 in group feat/1126-nodes-compose
2026-10-26 18:49:09 +01:00
jschoubben ebc1a84e59 Design 53 §13: the seed closes a milestone whose issues are all done, wontfix or resolved with no live fix included, as the tracker counts it (issue 1781)
mesh/delivery merged: source.main moved to novox/hq@d698942b1d56
mesh/delivery-group group feat/1781-milestone-records merged: every member is merged
mesh/check pass ebc1a84e592e on 132aae843383 | a compiled program is not refused: mesh-build-node reported no programs | unit 39 run, 0 kept, 1 skippe…
2026-10-26 18:40:12 +01:00
jschoubben 5bb109e36e Design 47: the node-record read is the composer copy's one intended grant beyond the controller's, and what a hand-back means for it (issue 1126, ADR 0555)
mesh/delivery closed: a newer head of the same pull request
mesh/check checking 5bb109e36ea7 on 515c405c8939 | novox/hq#482 in group feat/1126-nodes-compose
2026-10-26 18:33:46 +01:00
jschoubben d948774794 Design 47: the node record rides in the node's own have key (issue 1126, ADR 0555 §3) 2026-10-26 18:30:28 +01:00
jschoubben 68f8a8c198 Design 47: the found tunnel is decided at adoption; adoption and enrolled peers are read, not written (issue 1126, ADR 0555) 2026-10-26 18:26:53 +01:00
jschoubben b5b84fafc6 Design 47: config placement, as ADR 0348 words it (issue 1126) 2026-10-26 18:21:31 +01:00
jschoubben 0488bc1801 Design 47 says what a node composes from: placement with the node's decided facts and settings layers, and the node's own record (issue 1126, ADR 0549, ADR 0555) 2026-10-26 18:20:51 +01:00
jschoubben 973182cdf9 Design 53: milestones are records of the tracker, an issue's milestone is its own link, and the seed reads the design's table once (ADR 0493, issue 1781) 2026-10-26 18:00:18 +01:00
jschoubben 97d600650a Design 47: each issue in one milestone row, R4a's range written out, and row M1 for the milestone records, so the tracker's seed takes the table without a guess (issue 1781) 2026-10-26 18:00:18 +01:00
mesh-tools 515c405c89 Merge pull request 'Design 47 names the controller-exit milestones as rows; the glossary and playbook 07 name progress (issue 1947)' (#481) from feat/1947-progress into main 2026-10-26 16:42:26 +00:00
jschoubben a6e27687db Design 47: 1945 is L1's issue alone, and milestone 3 names 774 with 1126's rest carried by its note (issue 1947, review round 1)
mesh/delivery merged: source.main moved to novox/hq@515c405c8939
mesh/delivery-group group feat/1947-progress merged: every member is merged
mesh/check pass a6e27687db42 on 132aae843383 | unit 39 run, 0 kept, 1 skipped | integration 39 run, 0 kept, 1 skipped | composition passed in 2ms
An issue belongs to one milestone (ADR 0493 §2); R6 is delivered and named 1945 for parts
L1 now carries.
2026-10-26 17:03:14 +01:00
jschoubben b7dabd8ceb Design 47 gives its running milestones rows; the glossary and playbook 07 name progress (issue 1947)
The three milestones of the controller leaving build and delivery were prose only, so no
reader of the table saw the running work; 1209 and 971 were listed in two milestones
against ADR 0493 §2.
2026-10-26 16:40:12 +01:00
mesh-tools 3a5c4c7c4d Merge pull request 'Designs 47, 24 and 39 and the glossary follow ADR 0548, 0549, 0550, 0551 and ADR 0547's answers' (#480) from amend/0548-0551 into main 2026-10-26 12:34:59 +00:00
jschoubben 250c3ff605 Design 24: the pointer to 'Every secret lives in the vault' says above
mesh/delivery merged: source.main moved to novox/hq@3a5c4c7c4d9c
mesh/check pass 250c3ff605ab on 132aae843383 | unit 39 run, 0 kept, 1 skipped | integration 39 run, 0 kept, 1 skipped | composition passed
2026-10-26 13:33:23 +01:00
jschoubben a3f6e967c0 Design 24: the controller hands the vault only the covered accounts' values until the third milestone, and the break-glass recovery is below (review of ADR 0550's amendment) 2026-10-26 13:32:37 +01:00
jschoubben 11b0a450c3 Designs 47, 24 and 39 follow ADR 0548 to 0551: one check per set of heads with carried verdicts and merge turns, placement in the catalogue, every secret in the vault
Design 47 names ADR 0548 (a check runs to its end, a trunk move carries a verdict by its package digests, the hold and the merge turn), ADR 0551 (the hold and turns only while the check gates merges), ADR 0549 (placement files read by the indexer) and ADR 0550 (the vault signs and adopts the loaded hashes), and ADR 0547's answers on placement and signing. Design 24 gains every secret in the vault; design 39 moves grants and tokens into it. The glossary gains carried verdict, merge turn and placement file.
2026-10-26 13:29:30 +01:00
mesh-tools 122cda84b3 Merge pull request 'Playbooks 07 and 08: read the forge branch's head before trusting an agent's push' (#479) from pb/read-the-pushed-head into main 2026-10-26 10:03:59 +00:00
jschoubben be1bbe0a65 Playbooks 07 and 08: read the forge head before trusting a reported push, so a try never runs on a push that did not happen
mesh/delivery merged: source.main moved to novox/hq@122cda84b302
mesh/check pass be1bbe0a652a on 132aae843383 | unit 39 run, 0 kept, 1 skipped | integration 39 run, 0 kept, 1 skipped | composition passed
2026-10-26 11:02:44 +01:00
mesh-tools 585551722a Merge pull request 'Design 47 follows ADR 0547: node agents compose, the grants part composes who may say what, the bus node writes the user list, and the controller leaves build and delivery in three milestones (issue 1945)' (#478) from fix/1945-user-list-follows-r into main 2026-10-26 07:51:14 +00:00
jschoubben f1c50d3e01 Design 47: the assigner's acceptance test names its six rules, as ADR 0547 §4 leaves them; the bus node follows the controller's list until the third milestone, for hand-back; placement stays where it is for now (issue 1126)
mesh/delivery merged: source.main moved to novox/hq@585551722a4e
mesh/check pass f1c50d3e018c on 132aae843383 | unit 39 run, 0 kept, 1 skipped | integration 39 run, 0 kept, 1 skipped | composition passed in 2ms
2026-10-26 02:59:58 +01:00
jschoubben d70791ddee Design 47: sentences that still named the controller as composer, issuer or reader of grants follow ADR 0547 as amended 2026-10-26 02:58:12 +01:00
jschoubben 5bdeb8db83 Design 47: take-over's test as ADR 0547's insight states it 2026-10-26 02:57:45 +01:00
jschoubben d94f8a05a8 Design 47 follows ADR 0547: node agents compose their own modules, a grants part composes who may say what, the bus node writes the user list alone, and the controller leaves build and delivery in three milestones 2026-10-26 02:57:45 +01:00
mesh-tools fd0d712f7d Merge pull request 'Design 47 and the as-is delivery after release 6: acceptance tests on wants, as-is 04 and 13 as the mesh runs now (issue 1130)' (#477) from fix/1130-after-r6 into main 2026-10-26 00:39:21 +00:00
jschoubben de4c69d476 Name no node in design 47's test 10 proof: hq is public (issue 1130, review round 2)
mesh/delivery merged: source.main moved to novox/hq@fd0d712f7d97
mesh/check pass de4c69d47634 on 132aae843383 | a compiled program is not refused: mesh-build-node reported no programs | unit 39 run, 0 kept, 1 skippe…
2026-10-26 01:28:32 +01:00
jschoubben 9eb963d5f1 Give each restated acceptance test of design 47 its own proof, and add puts, the hand path and the builds bucket to as-is 04 (issue 1130, review round 1)
Tests 18 and 19 do not cover tests 3, 7, 9, 10 and 11 as restated, so each now names the test or live read that proves it, or says it is not yet proved.
2026-10-26 01:27:21 +01:00
jschoubben c255fd071c Describe delivery after release 6: as-is 04 and 13 follow wants, compositions and haves, design 47's acceptance tests restated (issue 1130)
Release 6 is delivered, so the as-is designs still describing the controller's plans were wrong,
and design 47's tests 1 to 12 named batches, plans and the merge window that no longer exist.
2026-10-26 01:21:42 +01:00
mesh-tools b11120f681 Merge pull request 'Playbooks 07 and 08: lessons from the 2026-10-25 recovery' (#476) from pb/recovery-lessons-2026-10-25 into main 2026-10-25 12:30:43 +00:00
jschoubben 0354947ba6 Playbooks 07 and 08: the dependent-pull-request rule leaves delivery group members out; the instance-search rule is a numbered item
mesh/delivery merged: source.main moved to novox/hq@b11120f681ef
mesh/check pass 0354947ba610 on b11120f681ef | unit 39 run, 0 kept, 1 skipped | integration 39 run, 0 kept, 1 skipped | composition passed
2026-10-25 13:29:17 +01:00
jschoubben 4d596bd211 Playbooks 07 and 08: open a dependent pull request only after delivery, fold a follow-up into the unapproved one, search every instance of a fault kind
mesh/delivery closed: a newer head of the same pull request
mesh/check pass 4d596bd21175 on 0e1d0392bc83 | unit 39 run, 0 kept, 1 skipped | integration 39 run, 0 kept, 1 skipped | composition passed
2026-10-25 13:27:54 +01:00
mesh-tools 0e1d0392bc Merge pull request 'R6: design 47, the glossary and playbook 07 follow the switch to wants (issue 1130)' (#475) from feat/r6 into main 2026-10-24 23:00:22 +00:00
jschoubben ba09497092 Name the hand-path command mesh-hand-compose, as mesh-sdk feat/r6 now calls it: main's mesh-compose is the composer (ADR 0482, issue 1943)
mesh/delivery-group group feat/r6 merged: every member is merged
mesh/delivery delivered
mesh/check pass ba09497092ec on 0e1d0392bc83 | unit 39 run, 0 kept, 1 skipped | integration 39 run, 0 kept, 1 skipped | composition passed
2026-10-24 23:06:00 +02:00
jschoubben 2d3a205740 Merge remote-tracking branch 'origin/main' into feat/r6 2026-10-24 23:00:09 +02:00
jschoubben 49dd8fc7e3 Merge pull request 'R4g finish: hq's checks as Go tests, the old word struck, the marker (issues 1296, 1935)' (#473) from feat/r4g-finish into main
Reviewed-on: #473
2026-10-24 20:56:42 +00:00
jschoubben 9558b71a4a Merge pull request 'Playbook 07: read a verdict's trunk against main at once, because a lagging source revision makes the Approve merge nothing (issue 1726)' (#474) from pb/verdict-trunk into main
Reviewed-on: #474
2026-10-24 19:47:56 +00:00
jschoubben 3dbb5d4e65 Design 47: the switch needs release 5's bus grants and the operator's key first, the record hold's exception after the switch, and nothing checks the budgets (review of 4b514e4, issue 1130) 2026-10-24 21:06:41 +02:00
jschoubben 4b514e4d69 Design 47, the glossary and playbooks 04, 07 and 08 for release 6: wants and signed compositions, no plans (ADR 0539, issue 1130)
Release 6 deletes delivery plans, batches, the merge window and tiers, so
design 47's sections that described them as current are rewritten to the
assigner's seven rules, the controller's signed compositions per node, the
node agent, credentials on a done build, the switch as release 5's last plan,
the hand path by mesh-compose and a want, and the view's delivered, moving,
settled and stuck events. The glossary gains composition and restates want,
declaration, send and apply; the playbooks follow a delivery by the view and
the have keys and call no plans, plan, push or what-if.
2026-10-24 21:01:24 +02:00
jschoubben 8433e42c03 Playbook 07: the glossary's words, the source revision and the gate (review)
mesh/delivery delivered
mesh/check pass 8433e42c03cd on 0e1d0392bc83 | unit 39 run, 0 kept, 1 skipped | integration 39 run, 0 kept, 1 skipped | composition passed
2026-10-24 13:54:31 +02:00
jschoubben 129af9906d Playbook 07: read a verdict's trunk against main at once, because a lagging trunk makes the Approve merge nothing (issue 1726) 2026-10-24 13:50:37 +02:00
jschoubben 2249d30151 Merge main into feat/r4g-finish (#472)
mesh/delivery-group group feat/r4g-finish merged: every member is merged
mesh/delivery delivered
mesh/check pass 2249d30151e8 on 0e1d0392bc83 | unit 39 run, 0 kept, 1 skipped | integration 39 run, 0 kept, 1 skipped | composition passed
2026-10-24 13:26:24 +02:00
jschoubben c12d503b2e Merge pull request 'Playbooks 07 and 08: ask who ends each bus key, how a switch-over goes one node first, how a group's integration level runs; a hand merge runs it first (research 071, issue 1941)' (#472) from pb/research-071-checks into main
Reviewed-on: #472
2026-10-24 11:10:51 +00:00
jschoubben 8498006d23 Playbooks 07 and 08: answer the review (no lab, plain words, checks on the forge, issue 1941) 2026-10-24 12:50:35 +02:00
jschoubben d20ed892cf Playbooks 07 and 08: three questions in the milestone file, and a try before a merge by hand (research 071, issue 1921) 2026-10-24 12:45:57 +02:00
jschoubben d08cac4e86 Answer the review of hq's finish: role names in the as-is delivery, ADR 0524-0529 named, stale check names fixed (issue 1296) 2026-10-24 04:40:02 +02:00
jschoubben ca460b681b hq's checks read the whole repository: mark the package reads-outside so its tests always run (issue 1935) 2026-10-24 03:46:56 +02:00
jschoubben b3b778a1a6 Describe delivery as it runs after R4g: the parts, the build modules, the one check (issue 1296, H9) 2026-10-24 03:41:01 +02:00
jschoubben 6dc008b5e7 Delete merge-check.sh and the four Python checks; the Go tests replace them (issue 1296, H6) 2026-10-24 03:39:11 +02:00
jschoubben 82aa3f6afa Merge pull request 'Playbooks 07 and 08: after a failed try, a local run of the integration level is a good first step (guideline)' (#471) from pb/try-fails-run-locally into main
Reviewed-on: #471
2026-10-23 21:50:24 +00:00
jschoubben 8055006ba1 Playbook 07: the two 'never run integration tests on the machine' lines say 'while developing', so the guideline after a failed try no longer contradicts them (review-471)
mesh/delivery delivered
mesh/check pass 8055006ba1f3 on 0e1d0392bc83 | unit 39 run, 0 kept, 1 skipped | integration 39 run, 0 kept, 1 skipped | over bound: unit (slowest unit…
2026-10-23 23:24:15 +02:00
jschoubben 2b53b6a5d2 Playbooks 07 and 08: say the local run after a failed try as a guideline, not a rule (the operator)
mesh/delivery superseded: a newer head of the same pull request
mesh/check pass 2b53b6a5d24a on 656f3af86f37 | unit 0 run, 39 kept | integration 0 run, 39 kept | clone 13s | composition passed with v0.1.14-0.20
2026-10-23 23:22:21 +02:00
jschoubben 87c338da8e Playbooks 07 and 08: a failed try or check is run locally at once, so every failure shows in one run, not one per try (the operator, 2026-10-23)
mesh/delivery superseded: a newer head of the same pull request
mesh/check pass 87c338da8eba on 656f3af86f37 | unit 39 run, 0 kept, 1 skipped | integration 39 run, 0 kept, 1 skipped | composition passed
2026-10-23 23:20:27 +02:00
jschoubben 656f3af86f Merge pull request 'R4g+R5 (hq): its checks as Go tests; design 47, the glossary and playbooks say what R4g builds (design 47)' (#470) from feat/r4g into main
Reviewed-on: #470
2026-10-23 17:54:31 +00:00
jschoubben e8bd9171cd Design 47: the gate reads mesh/check, the forge requires no status (ADR 0534 §1 changes ADR 0447 §5)
mesh/repo-check head e8bd9171cdbc merged into trunk fa7629d02be7 — pass: its merge-check.sh passed
mesh/delivery delivered
mesh/delivery-group group feat/r4g merged: every member is merged
mesh/check pass e8bd9171cdbc on 0e1d0392bc83 | unit 39 run, 5 kept, 1 skipped | integration 39 run, 12 kept, 1 skipped | composition passed
2026-10-23 16:31:15 +02:00
jschoubben 8f7c23da7d Make a timer that writes state, or a rule in code that overrides a person, a change the review asks (issue 1903) 2026-10-23 16:10:44 +02:00
jschoubben d49dccb8d5 Use the glossary's word composition check where the merge gate was struck (issue 1303) 2026-10-23 16:10:08 +02:00
jschoubben 6ead37ee7b Design 47: no status is required on any repository, so hq's Python checks go after the delivery (ADR 0534), and ADR 0529 is named 2026-10-23 16:10:01 +02:00
jschoubben c8ccbfe35f Playbooks 07, 08 and the overview name what runs: the checker's try and status, hq's Go tests, the stream backlog, and the way to deliver without the builder (issue 1888) 2026-10-23 15:17:30 +02:00
jschoubben 99fa8f0f09 Glossary: the build seat, the build queue and the build capacity as asks on a language's streams, and the build module (issue 1788) 2026-10-23 15:16:08 +02:00
jschoubben 75056fa931 Design 47 says what R4g and R5 build as one milestone: asks on streams, the assigner's wants, hq's Go tests, the hand path (issue 1130) 2026-10-23 15:15:18 +02:00
jschoubben 47e78d8ff4 Strike the word merge gate: the composition check replaces it in the glossary, the designs and the playbook, and old issues keep it dated (issue 1303) 2026-10-23 15:12:57 +02:00
jschoubben 1385452dc9 Say in the checks' README that the same four checks are Go tests (issue 1296) 2026-10-23 15:11:20 +02:00
jschoubben 5b63a86464 Run words.py as Go tests, with the clock injected and the catalogue comparison skipped when unset (issue 1296) 2026-10-23 15:10:53 +02:00
jschoubben 344c92498d Run cycle.py as Go tests: a design's layer, status, decisions and code, and the issues folder's numbers and frontmatter (issue 1296) 2026-10-23 15:07:48 +02:00
jschoubben 9339e953b8 Run index.py as Go tests: the reading order's topics are written and no list of records is stored (issue 1296) 2026-10-23 15:07:48 +02:00
jschoubben 0ad95768b6 Run records.py's five checks as Go tests, each proven against a broken tree first (issue 1296) 2026-10-23 15:07:35 +02:00
jschoubben dd45f31307 Give hq a Go module and the checks' shared reading, so the four checks can become Go tests (issue 1296) 2026-10-23 15:06:25 +02:00
mesh-tools fa7629d02b Merge pull request 'Design 42: the package manager and kernel rows as ADR 0507, 0508 and 0510 decide (upgrade safety)' (#467) from feat/upgrade-safety into main 2026-10-22 18:34:11 +00:00
mesh-tools 93963aeb51 Merge pull request 'Playbooks: how a module's old directory is closed, and a refused try (issues 1843, 1854)' (#468) from pb/moving-a-module-directory into main 2026-10-22 18:19:19 +00:00
mesh-tools 795b3b5d45 Merge pull request 'Design 63 (the mail web UI with mail reading and Owly) and playbook 10 (visual design)' (#469) from design-63-mail-web-ui into main 2026-10-22 18:16:54 +00:00
jochen 030e985694 Answer the review's second round: the mockup's sheet holds invented sales figures; milestone 5's row lists only teaching and the counts
mesh/merge-gate head 030e9856940d merged into trunk 58555ec33074 — pass: the change touches no module of the mesh's graph
mesh/repo-check head 030e9856940d merged into trunk 58555ec33074 — pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-22 19:21:38 +02:00
jochen eddd7ba684 Answer the review: mockup data invented throughout, later milestones' scope marked proposed, design 59 says where it is replaced, playbook 10 aligned with 04 and 07 2026-10-22 19:19:52 +02:00
jochen 5d745d04db Write design 63 and playbook 10, so the mail page's next steps are built from a picked design
Design 63 turns the mail page into the operator's list with Owly beside it (ADR 0515
to 0519), in six milestones with their issues; its reference mockup is kept beside it,
its data invented. Playbook 10 says how a page is designed before it is built: mockups
with fixed data, the operator picks, the pick is the reference. The as-is of the mail
modules now describes mail reading as delivered, the glossary gains design 63's words,
design 59's milestone 4 is marked replaced, and AGENTS.md allows a design's mockup.

Issues 1855 and 1856.
2026-10-22 18:42:42 +02:00
jochen 7fcca88c01 Playbooks: how a module's old directory is closed, and a refused try (issues 1843, 1854)
mesh/merge-gate head 7fcca88c012e merged into trunk 795b3b5d4593 — pass: the change touches no module of the mesh's graph (run 2)
mesh/repo-check head 7fcca88c012e merged into trunk 795b3b5d4593 — pass: its merge-check.sh passed (run 2)
mesh/delivery delivered
Lessons from closing only-office's old directories: a directory keeps its id until emptied on the
operator's word, then leaves the manifest in a second fix; and a fifth try is refused, not queued.
2026-10-22 18:25:05 +02:00
jschoubben 333e73830c Amend design 42 for the package view, one-at-a-time upgrades and the fallback kernel (ADR 0507, ADR 0508, ADR 0510; issues 1840, 1841)
mesh/repo-check head 333e73830c31 merged into trunk 93963aeb5189 beside mesh-catalog#450@86ba54b0 +1 more — pass: its merge-check.sh passed
mesh/merge-gate head 333e73830c31 merged into trunk 93963aeb5189 — pass: group feat/upgrade-safety composed: every machine composes with the change as it d…
mesh/delivery delivered
mesh/delivery-group group feat/upgrade-safety merged: every member is merged
2026-10-22 17:41:15 +02:00
mesh-tools 58555ec330 Merge pull request 'Name the session that pushed a head in its pull request (ADR 0505 §5)' (#466) from feat/quiet-deliveries into main 2026-10-22 14:01:16 +00:00
jschoubben 9a74de0bd6 Name the session that pushed a head in its pull request, so a head that needs a new one names its author (ADR 0505 §5)
mesh/repo-check head 9a74de0bd64f merged into trunk 77d4a8a39e1f beside mesh-catalog#444@09e749bc +1 more — pass: its merge-check.sh passed
mesh/merge-gate head 9a74de0bd64f merged into trunk 77d4a8a39e1f — pass: group feat/quiet-deliveries composed: every machine composes with the change as it…
mesh/delivery delivered
mesh/delivery-group group feat/quiet-deliveries merged: every member is merged
2026-10-22 14:25:50 +02:00
mesh-tools 77d4a8a39e Merge pull request 'Never cut a milestone by an order between its parts; the delivery plan keeps that order' (#465) from process/07-order-is-the-plans into main 2026-10-21 23:44:17 +00:00
jschoubben 2b857df4b5 Never cut a milestone by an order between its parts: the delivery plan keeps that order from the module graph
mesh/merge-gate head 2b857df4b51e merged into trunk 73a06324dc0d — pass: the change touches no module of the mesh's graph
mesh/repo-check head 2b857df4b51e merged into trunk 73a06324dc0d — pass: its merge-check.sh passed
mesh/delivery delivered
The operator, 2026-10-22: a milestone is a milestone; which module reaches the machines first is what the delivery plan is for. The prepare step still lists every part a change reaches, as issues of the same milestone; an order the plan cannot keep is an issue of the build and delivery system.
2026-10-22 01:30:20 +02:00
mesh-tools 73a06324dc Merge pull request 'Correct design 47 after the review of #463' (#464) from design/47-cites-fixes into main 2026-10-21 21:15:04 +00:00
jschoubben c07483081d Cite ADR 0494 in design 47: the checker's milestone is one group, not two deliveries
mesh/merge-gate head c07483081db1 merged into trunk 57396112c7ab — pass: the change touches no module of the mesh's graph
mesh/repo-check head c07483081db1 merged into trunk 57396112c7ab — pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-21 22:54:18 +02:00
jschoubben 1955d7f624 Correct design 47 after review of #463: R4e timing of the SDK group, the wait list's Waited-by condition, and what builds images from R4g
mesh/merge-gate waiting in the build queue since 22:52
mesh/delivery superseded: a newer head of the same pull request
2026-10-21 22:52:18 +02:00
mesh-tools 57396112c7 Merge pull request 'Name the accepted decisions 0480 to 0493 in design 47 and state what they changed' (#463) from design/47-cites-0484-0493 into main 2026-10-21 20:49:11 +00:00
jschoubben 38020bee9a Name the accepted decisions 0480 to 0493 in design 47 and state in its prose what they changed, so the design matches what holds
mesh/merge-gate head 38020bee9a0c merged into trunk 04fff52d3b42 — pass: the change touches no module of the mesh's graph
mesh/repo-check head 38020bee9a0c merged into trunk 04fff52d3b42 — pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-21 22:44:29 +02:00
mesh-tools 04fff52d3b Merge pull request 'Design 36: the plugin's session-start and Stop hooks beside the guard, and the waits mirror in the agent's home (ADR 0489, ADR 0490)' (#462) from design/36-wait-list-hooks into main 2026-10-21 17:53:42 +00:00
jochen ac72fe6422 Design 36: the waits mirror's file is named by Claude Code's session id
mesh/merge-gate head ac72fe642229 merged into trunk a2147766963b — pass: the change touches no module of the mesh's graph
mesh/repo-check head ac72fe642229 merged into trunk a2147766963b — pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-21 19:30:21 +02:00
jochen 9483caae2d Design 36: the waits mirror is the module's own entry in the agent's home; the hook tools refuse mesh and node scope
Review of the hooks paragraph: the mirror in the agent's home was not in the home's table, so the home look would count it as drift.
2026-10-21 19:29:38 +02:00
jochen 302ab2856a Design 36: the plugin's session-start and Stop hooks beside the guard (ADR 0489, ADR 0490)
Delivered in mesh-catalog#420 on 2026-10-21; the design said the guard was the plugin's only hook.
2026-10-21 19:27:33 +02:00
mesh-tools a214776696 Merge pull request 'Playbook 07: a new verb of a seat the mesh defines waits for the SDK and a controller delivered with it' (#460) from playbook/seat-contract-order into main 2026-10-21 16:14:23 +00:00
mesh-tools ac7e445e7a Merge pull request 'Playbooks 07 and 08: a milestone is one delivery group released by one plan; a fix found during it rides in the group' (#461) from playbook/08-release-per-milestone into main 2026-10-21 16:13:04 +00:00
jschoubben 0162e04f92 Describe a milestone as one delivery group released by one plan, and make a fix found during it ride in the group
mesh/merge-gate head 0162e04f9275 merged into trunk d5d821220494 — pass: the change touches no module of the mesh's graph
mesh/repo-check head 0162e04f9275 merged into trunk d5d821220494 — pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-21 18:08:58 +02:00
jschoubben 37a01b93a5 Use the glossary's word for the milestone's branch
mesh/merge-gate building on ace since 18:08
mesh/delivery superseded: a newer head of the same pull request
2026-10-21 18:05:07 +02:00
jschoubben c6fc663019 Make a small fix in mesh-sdk or mesh-controller ride with the milestone's release, so each is delivered once per milestone 2026-10-21 18:04:49 +02:00
jschoubben 5f281dd2a3 Playbook 07: a new verb of a seat the mesh defines waits for the SDK and a controller delivered with it, so the prepare step searches the SDK for each seat a change adds verbs to
mesh/merge-gate head 5f281dd2a3f4 merged into trunk ac7e445e7ae9 — pass: the change touches no module of the mesh's graph
mesh/repo-check head 5f281dd2a3f4 merged into trunk ac7e445e7ae9 — pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-21 15:22:27 +02:00
mesh-tools d5d8212204 Merge pull request 'Playbooks 07 and 08: the session that opens an issue along the way fixes it' (#459) from playbook/fix-what-you-open into main 2026-10-21 12:47:52 +00:00
jochen aa56e9b9e7 Playbook 07: the fixed scope and fixing what you open agree, a blocking issue still goes in
mesh/merge-gate head aa56e9b9e7c5 merged into trunk abdd364f091d — pass: the change touches no module of the mesh's graph
mesh/repo-check head aa56e9b9e7c5 merged into trunk abdd364f091d — pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-21 14:42:27 +02:00
jochen 4f8c469329 Playbooks 07 and 08: the session that opens an issue along the way fixes it
The operator, 2026-10-21: issues opened along the way were only listed and left. Small ones are fixed in between as small fixes, the rest right after the delivery, outside the running milestone's branch, so its fixed scope holds.
2026-10-21 14:40:56 +02:00
mesh-tools abdd364f09 Merge pull request 'Playbook 07: name a pull request's own issues as 'issue <n>', and no other' (#458) from playbook/pr-issue-words into main 2026-10-21 12:17:55 +00:00
jochen ad0a95dc0c Playbook 07: the plural 'issues <n>' is read too; 1654 was another milestone's issue
mesh/merge-gate head ad0a95dc0c1b merged into trunk ab3ac686dcdd — pass: the change touches no module of the mesh's graph
mesh/repo-check head ad0a95dc0c1b merged into trunk ab3ac686dcdd — pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-21 14:06:36 +02:00
jochen c770bc1c50 Playbook 07: name a pull request's own issues as 'issue <n>', and no other
mesh-delivery reads a pull request's issues from the words 'issue <n>' in its description; okioki #9 listed its own bare and named two follow-ups that way, and the Approve's ask named the follow-ups.
2026-10-21 14:05:28 +02:00
mesh-tools ab3ac686dc Merge pull request 'Correct design 47's R4c row (installed toolchain) and move 1305 from R4e to R4f' (#457) from design/47-r4c-row into main 2026-10-21 11:40:39 +00:00
jschoubben 373b7e7dd1 Correct design 47's R4c row (installed toolchain, ADR 0477/0478) and move 1305 from R4e to R4f
mesh/delivery delivered
mesh/merge-gate out of date: judged against trunk 6eda56470131, the trunk is now ab3ac686dcdd; judged again on a recheck or when it is merged
mesh/repo-check out of date: judged against trunk 6eda56470131, the trunk is now ab3ac686dcdd; judged again on a recheck or when it is merged
2026-10-21 13:35:58 +02:00
mesh-tools 6eda564701 Merge pull request 'Add milestone F2b to design 60, and make every strict reader of manifests a release order in playbook 07' (#456) from design/60-milestone-f2b into main 2026-10-21 11:00:57 +00:00
jschoubben 7c8e1f521d Add milestone F2b to design 60, and write into playbook 07's prepare step that every strict reader of manifests ships a new word first
mesh/merge-gate head 7c8e1f521db9 merged into trunk a1188b64a431 — pass: the change touches no module of the mesh's graph
mesh/repo-check head 7c8e1f521db9 merged into trunk a1188b64a431 — pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-21 12:53:37 +02:00
mesh-tools a1188b64a4 Merge pull request 'Mark design 61 designed now that ADR 0479 is accepted, and follow its last version' (#455) from design/61-adr-0479-accepted into main 2026-10-21 10:40:20 +00:00
mesh-tools 6db031f486 Merge pull request 'Design 62: name ADR 0481 accepted, and say the page reaches OkiOki only over the bus' (#454) from design/62-review-fixes into main 2026-10-21 10:39:21 +00:00
jschoubben be30b63976 Mark design 61 designed now that ADR 0479 is accepted, and follow its last version: PDF and JPEG only, the printer asks its own link
mesh/merge-gate head be30b63976de merged into trunk 6db031f486f4 — pass: the change touches no module of the mesh's graph
mesh/repo-check head be30b63976de merged into trunk 6db031f486f4 — pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-21 12:36:24 +02:00
jochen 993c7def3a Design 62: name ADR 0481 accepted, and say the page reaches OkiOki only over the bus
mesh/merge-gate head 993c7def3ae3 merged into trunk a2918df0aab2 — pass: the change touches no module of the mesh's graph
mesh/repo-check head 993c7def3ae3 merged into trunk a2918df0aab2 — pass: its merge-check.sh passed
mesh/delivery delivered
The review of hq#453 after its merge found the design calling ADR 0481 proposed, and the operator's rule (the page uses the bus and makes no OkiOki call, 2026-10-21) only implied. A document crosses the bus in parts, as scanner.document does.
2026-10-21 12:32:31 +02:00
mesh-tools a2918df0aa Merge pull request 'Design 62: the bookkeeping portal (ADR 0481)' (#453) from design/62-bookkeeping-portal into main 2026-10-21 10:31:02 +00:00
jochen 46d6840c67 Design 62: the page is read-only and its reach internal before assignment (ADR 0481 v4)
mesh/merge-gate head 46d6840c67f2 merged into trunk 82343be78f51 — pass: the change touches no module of the mesh's graph
mesh/repo-check head 46d6840c67f2 merged into trunk 82343be78f51 — pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-21 11:56:03 +02:00
jochen b717900700 Design 62: the bookkeeping portal (ADR 0481)
The operator asked for a page showing the business's income, expenses and to-do as the
bookkeeping service holds them now, on their own hardware: okioki moves whole to the home
server and okioki-web runs beside it, behind Keycloak, never changing what the accountant did.
2026-10-21 11:55:05 +02:00
mesh-tools 82343be78f Merge pull request 'Write design 61, the print and scan page, as the home of ADR 0479' (#452) from design/61-print-and-scan into main 2026-10-21 09:32:15 +00:00
mesh-tools 75f2cbb6d6 Merge pull request 'Name the accepted ADR 0468 in design 60 and break its building into three milestones' (#451) from design/60-adr-0468 into main 2026-10-21 09:27:16 +00:00
jschoubben 467a2e6c97 Credit restart and the settings read to ADR 0467, and only configure to ADR 0474
mesh/merge-gate head 467a2e6c9785 merged into trunk 75f2cbb6d6ba — pass: the change touches no module of the mesh's graph
mesh/repo-check head 467a2e6c9785 merged into trunk 75f2cbb6d6ba — pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-21 11:18:56 +02:00
jschoubben 115df9fd69 Follow ADR 0479 version 6: the glass only until the feeder is decided, and how the upload bound holds 2026-10-21 11:18:13 +02:00
jschoubben c7bebf53d4 Answer the review: the page follows ADR 0474, and records who managed the printer 2026-10-21 11:16:48 +02:00
jschoubben e0118ca67e Write design 61, the print and scan page, as the home of ADR 0479 2026-10-21 11:15:36 +02:00
jschoubben 64e64ac69d Answer the review: credit the one-call print to ADR 0468, name F3's dependency and the parts verbs' exception
mesh/merge-gate head 64e64ac69dff merged into trunk a0ab91dcae1d — pass: the change touches no module of the mesh's graph
mesh/repo-check head 64e64ac69dff merged into trunk a0ab91dcae1d — pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-21 11:12:24 +02:00
jschoubben e111fb875c Name the accepted ADR 0468 in design 60 and break its building into three milestones, cut by release order 2026-10-21 11:10:31 +02:00
mesh-tools a0ab91dcae Merge pull request 'Playbook 08: check a module compiles with go vet, never go build in its folder (issue 1577)' (#450) from fix/1577-no-build-in-module into main 2026-10-21 08:02:23 +00:00
jochen 9f0b0215ae Playbook 08: check a module compiles with go vet, never go build in its folder (issue 1577)
mesh/merge-gate head 9f0b0215ae6a merged into trunk 86fd4adf9756 — pass: the change touches no module of the mesh's graph
mesh/repo-check head 9f0b0215ae6a merged into trunk 86fd4adf9756 — pass: its merge-check.sh passed
mesh/delivery delivered
go build in a module's folder writes the program beside go.mod, and two such programs were
committed on 2026-10-21, caught only by the review.
2026-10-21 09:58:44 +02:00
mesh-tools 86fd4adf97 Merge pull request 'Correct design 60: status proposed, each brother feature tied to the record that decides it (ADR 0474, ADR 0475)' (#449) from design/60-the-brother-module into main 2026-10-20 23:57:54 +00:00
jschoubben 0b21bd0b26 Name ADR 0475 in design 60, now its number is reserved
mesh/merge-gate head 0b21bd0b268f merged into trunk fa165392b12d — pass: the change touches no module of the mesh's graph
mesh/repo-check head 0b21bd0b268f merged into trunk fa165392b12d — pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-21 01:55:22 +02:00
jschoubben e212e3841d Follow the scanning record's settled shape in design 60: a file under the home folder, the certificate as health 2026-10-21 01:54:01 +02:00
jschoubben 8d3a9011ac Mark design 60 proposed and tie each brother feature to the record that decides it
The review found features stated as in force that rest on records still proposed
(ADR 0468, ADR 0474 and the scanning record to be reserved), and rules without
the test that checks them.
2026-10-21 01:47:59 +02:00
mesh-tools fa165392b1 Merge pull request 'Design the brother module whole: scanning, the feeder, printing, settings read and configure' (#448) from design/60-the-brother-module into main 2026-10-20 23:46:03 +00:00
jschoubben b6e01318cf Design the brother module whole, so printer settings can be changed through the mesh without opening the device
mesh/merge-gate head b6e01318cff2 merged into trunk 80653c078de5 — pass: the change touches no module of the mesh's graph
mesh/repo-check head b6e01318cff2 merged into trunk 80653c078de5 — pass: its merge-check.sh passed
mesh/delivery delivered
The operator asked on 2026-10-21 to configure the printer and scanner easily, or through
the mesh's tools; the design names the configure verb, its deny list and the proposed
decision it rests on (ADR 0474, number to be reserved).
2026-10-21 01:43:14 +02:00
mesh-tools 80653c078d Merge pull request 'Playbook 07: read a hand push's plan first, and assign a new module to one machine first' (#447) from playbook-07/hand-push-and-first-assignment into main 2026-10-20 23:38:01 +00:00
mesh-tools ba9f063c77 Read a hand push's plan first, and assign a new module to one machine first, from the night's build-module faults
mesh/merge-gate head ba9f063c7761 merged into trunk 70eff93d0130 — pass: the change touches no module of the mesh's graph
mesh/repo-check head ba9f063c7761 merged into trunk 70eff93d0130 — pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-20 23:34:42 +00:00
mesh-tools 70eff93d01 Merge pull request 'Design 59: no grant split (ADR 0472), and milestone 2's issues as split at its prepare step' (#446) from design-59-no-grant-split into main 2026-10-20 21:43:16 +00:00
jochen 9a9934d47e Design 59: say each milestone 2 issue's order, as the tracker holds it
mesh/merge-gate head 9a9934d47ecc merged into trunk 34e3321c7fcf — pass: the change touches no module of the mesh's graph
mesh/repo-check head 9a9934d47ecc merged into trunk 34e3321c7fcf — pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-20 23:37:14 +02:00
jochen 7f3a3113aa Design 59: no grant split (ADR 0472), and milestone 2's issues as split at its prepare step 2026-10-20 23:35:53 +02:00
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
mesh-tools f0bcc75fcf Merge pull request 'Say in the as-is of bus logins that a module's process may have a login of its own (ADR 0436, ADR 0438)' (#430) from docs/process-accounts-as-is into main 2026-10-19 09:06:33 +00:00
jochen 550487fc90 Say in the as-is of bus logins that a module's process may have a login of its own (ADR 0436, ADR 0438)
mesh/merge-gate head 550487fc9018 merged into trunk 022392a22bb1 — pass: the change touches no module of the mesh's graph
mesh/repo-check head 550487fc9018 merged into trunk 022392a22bb1 — pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-19 11:02:57 +02:00
292 changed files with 6227 additions and 2250 deletions
+1 -1
View File
@@ -51,7 +51,7 @@ Decide the shape first:
``` ```
then `# NNN. The title`, the title given to the tracker. **No status key**: the issue's status, owner and fix then `# NNN. The title`, the title given to the tracker. **No status key**: the issue's status, owner and fix
live in the tracker (ADR 0275), and `cycle.py` refuses `status:`, `located-in:`, `fixed-by:`, live in the tracker (ADR 0275), and the cycle test refuses `status:`, `located-in:`, `fixed-by:`,
`amended-design:`, `replay:` or `replay-none:` in any report. Notes and links go to the `amended-design:`, `replay:` or `replay-none:` in any report. Notes and links go to the
tracker. tracker.
4. Write the symptom **as observed**, in plain terms, with the evidence that it happened. 4. Write the symptom **as observed**, in plain terms, with the evidence that it happened.
+1 -1
View File
@@ -44,6 +44,6 @@ generated on demand and never written back to disk.
always generated, always ephemeral. This includes the ADR index — the hand-written one had always generated, always ephemeral. This includes the ADR index — the hand-written one had
already drifted after a single addition, which is why it was removed, and the generated one already drifted after a single addition, which is why it was removed, and the generated one
written into `02-DECISIONS/README.md` made every two open decisions conflict (issue 297), which written into `02-DECISIONS/README.md` made every two open decisions conflict (issue 297), which
is why that went too. `index.py` fails if the list is stored there again. is why that went too. the index test fails if the list is stored there again.
- Do not infer status from prose. Trust only a design's frontmatter, the tracker for an issue and the - Do not infer status from prose. Trust only a design's frontmatter, the tracker for an issue and the
knowledge base for a record; if one is wrong, flag it. knowledge base for a record; if one is wrong, flag it.
+1 -1
View File
@@ -15,7 +15,7 @@ works — plus the engineering practice that holds across everything Novox build
## Rules ## 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 - **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 exception is `how-we-build.md`, which changes whenever a rule is earned — and only through
its amendment process. its amendment process.
+20 -16
View File
@@ -1,15 +1,19 @@
# Checks # Checks
The four checks are Go tests (ADR 0527), in this folder (`*_test.go`) with `go.mod` at the root: run
`go test ./...` from the root. The checker runs the same as `mesh/check` on every pull request. Each check is a
function over a file tree that returns its problems, tested on hq's tree and on a deliberately broken tree built
in memory. The Python files and `merge-check.sh` are gone (issue 1296).
``` ```
python3 00-META/checks/records.py structure: links, no record written here, citations in the form "ADR NNNN" records_test.go structure: links, no record written here, citations in the form "ADR NNNN"
python3 00-META/checks/index.py the reading order's topics are written, and no list of records is stored index_test.go the reading order's topics are written, and no list of records is stored
python3 00-META/checks/cycle.py the development cycle: a design's layer, status, decisions and code; an issue report's frontmatter cycle_test.go the development cycle: a design's layer, status, decisions and code; an issue report's frontmatter
python3 00-META/checks/words.py the glossary's retired words are not used, no daemon's line is written, and no word is defined twice words_test.go the glossary's retired words are not used, no daemon's line is written, and no word is defined twice
python3 00-META/checks/words.py --list tools the words the catalogue's copy must list
python3 00-META/checks/words.py --list lines the daemons' lines the catalogue's copy must list
``` ```
Non-zero exit on any problem, so it can be a gate rather than a report. A failing test fails `go test`, so it can be a gate rather than a report. With `MESH_CATALOG_DIR` set, the words
test also compares the catalog's copies of the retired words and the daemons' lines with the glossary.
**Why this exists.** Until now nothing in this repository was verified by anything but reading, **Why this exists.** Until now nothing in this repository was verified by anything but reading,
which is how a superseded decision stayed live in the constitution for days and in which is how a superseded decision stayed live in the constitution for days and in
@@ -26,7 +30,7 @@ indistinguishable from one that cannot.
| `no-records` | `02-DECISIONS/` and `01-RESEARCH/` hold their README and nothing else: a decision record or research effort lives in the knowledge base (ADR 0290 rule 9) | written with the change that removed the records (issue 426); it failed on a record file put back in `02-DECISIONS/` before it passed | | `no-records` | `02-DECISIONS/` and `01-RESEARCH/` hold their README and nothing else: a decision record or research effort lives in the knowledge base (ADR 0290 rule 9) | written with the change that removed the records (issue 426); it failed on a record file put back in `02-DECISIONS/` before it passed |
| `decisions-form` | every entry of a design's `decisions:` is "ADR NNNN", four digits | written with issue 426; it failed on an entry `ADR 290` before it passed | | `decisions-form` | every entry of a design's `decisions:` is "ADR NNNN", four digits | written with issue 426; it failed on an entry `ADR 290` before it passed |
| `mention-form` | every "ADR N" in the text has four digits, and every "research N" three; a code span is an example and is not read | written with issue 426; it failed on `ADR 12` and `research 45` before it passed | | `mention-form` | every "ADR N" in the text has four digits, and every "research N" three; a code span is an example and is not read | written with issue 426; it failed on `ADR 12` and `research 45` before it passed |
| *(index.py)* | the reading order's six topics are written in `02-DECISIONS/README.md`, none twice, and the README stores no list of records (ADR 0248). The knowledge base reads these lines and refuses a record whose topic is not one of them | a stored list made every two open decisions conflict ([issue 297](../../04-ISSUES/297-two-open-decisions-always-conflict/00-report.md)); it failed on the README of `main` before the list was removed | | *(index test)* | the reading order's six topics are written in `02-DECISIONS/README.md`, none twice, and the README stores no list of records (ADR 0248). The knowledge base reads these lines and refuses a record whose topic is not one of them | a stored list made every two open decisions conflict ([issue 297](../../04-ISSUES/297-two-open-decisions-always-conflict/00-report.md)); it failed on the README of `main` before the list was removed |
| `status-vs-code` | a to-be document naming specific code is not still `designed` | **ten documents**, several with a *What was built* section, describing lab-proven code | | `status-vs-code` | a to-be document naming specific code is not still `designed` | **ten documents**, several with a *What was built* section, describing lab-proven code |
## Where the rules on records went ## Where the rules on records went
@@ -37,7 +41,7 @@ named in its refusal: `knowledge-base.propose` refuses a record that breaks one
record, naming every reason at once, and the four rules that judge the whole record set run after every record, naming every reason at once, and the four rules that judge the whole record set run after every
acceptance and raise a condition when one fails (mesh-knowledge's `rules.go`, one table-test case per rule). acceptance and raise a condition when one fails (mesh-knowledge's `rules.go`, one table-test case per rule).
| Rule here until issue 426 | Where it lives now | | Rule here until issue 426 (the Python checks, deleted with issue 1296) | Where it lives now |
|---|---| |---|---|
| `records.py` `rests-on`, for a record's `extends:` | the knowledge base's `rests-on` at propose, and the whole-set rule `rests-on-live` | | `records.py` `rests-on`, for a record's `extends:` | the knowledge base's `rests-on` at propose, and the whole-set rule `rests-on-live` |
| `records.py` `rests-on`, for a design's `decisions:` naming an accepted record | **nowhere yet**: this repository cannot read the knowledge base. The reviewer checks each new entry with `knowledge-base.get`. Designs become the knowledge base's own kind under issue 428 | | `records.py` `rests-on`, for a design's `decisions:` naming an accepted record | **nowhere yet**: this repository cannot read the knowledge base. The reviewer checks each new entry with `knowledge-base.get`. Designs become the knowledge base's own kind under issue 428 |
@@ -47,7 +51,7 @@ acceptance and raise a condition when one fails (mesh-knowledge's `rules.go`, on
| `records.py` `partial-supersession` | the knowledge base's `changes` link to a clause, `changes-in-force` and `supersedes-accepted`, and the whole-set rule `partial` | | `records.py` `partial-supersession` | the knowledge base's `changes` link to a clause, `changes-in-force` and `supersedes-accepted`, and the whole-set rule `partial` |
| `records.py` `clauses` | the knowledge base's `has-clause`, `clause-numbering`, `clause-lead` and `clause-size` | | `records.py` `clauses` | the knowledge base's `has-clause`, `clause-numbering`, `clause-lead` and `clause-size` |
| `records.py` `numbering` | the knowledge base's `title` and `reserved` | | `records.py` `numbering` | the knowledge base's `title` and `reserved` |
| `records.py` `topics`, `index.py` (a record's topic) | the knowledge base's `topic`, against the reading order `index.py` still checks | | `records.py` `topics`, `index.py` (a record's topic) | the knowledge base's `topic`, against the reading order the index test still checks |
| `records.py` `insights` | the knowledge base's `insight-form`; an accepted record changes by a new version marked as a progressive insight | | `records.py` `insights` | the knowledge base's `insight-form`; an accepted record changes by a new version marked as a progressive insight |
| `index.py --print` | `knowledge-base.holds` with a topic | | `index.py --print` | `knowledge-base.holds` with a topic |
| `cycle.py` research: a known status, `became:` when graduated | the knowledge base's `research-status`, `overview` and `became` | | `cycle.py` research: a known status, `became:` when graduated | the knowledge base's `research-status`, `overview` and `became` |
@@ -72,7 +76,7 @@ acceptance and raise a condition when one fails (mesh-knowledge's `rules.go`, on
State what incident it would have caught, and make it fail before you make it pass. A check State what incident it would have caught, and make it fail before you make it pass. A check
whose failure has never been observed is a guess about its own correctness. whose failure has never been observed is a guess about its own correctness.
## cycle.py ## The cycle test
The development cycle, checked (ADR 0080): The development cycle, checked (ADR 0080):
every design parses, its `layer:` matches its folder and its `status:` is known; a to-be design names a every design parses, its `layer:` matches its folder and its `status:` is known; a to-be design names a
@@ -84,9 +88,9 @@ and no status key — `status:`, `located-in:`, `fixed-by:`, `amended-design:`,
that carried them before they were removed. The rules on an issue's status this check held until then — an that carried them before they were removed. The rules on an issue's status this check held until then — an
owner for a located issue, a fix for a resolved one, and a core issue's `replay` or `replay-none` (ADR 0237) owner for a located issue, a fix for a resolved one, and a core issue's `replay` or `replay-none` (ADR 0237)
— are refused at the write by `issue-tracker.move`. Its rules on research efforts and decision records left — are refused at the write by `issue-tracker.move`. Its rules on research efforts and decision records left
with them (the table above). `python3 00-META/checks/cycle.py` with them (the table above).
## words.py ## The words test
One word per thing, checked (ADR 0244). One word per thing, checked (ADR 0244).
It reads the glossary's *Not:* lines (the retired words, each with its scope) and *Identifier until It reads the glossary's *Not:* lines (the retired words, each with its scope) and *Identifier until
@@ -95,15 +99,15 @@ quotations, struck-through text, link targets, comments and frontmatter are take
`03-DESIGN/`, `AGENTS.md`, `README.md`, and issues opened from 2026-10-07. Decision records and research `03-DESIGN/`, `AGENTS.md`, `README.md`, and issues opened from 2026-10-07. Decision records and research
efforts live in the knowledge base, which holds a proposed record to the same words (the table above). It efforts live in the knowledge base, which holds a proposed record to the same words (the table above). It
also fails when a glossary head word heads two entries or is also retired. also fails when a glossary head word heads two entries or is also retired.
[`words-allowed.md`](words-allowed.md) names a document that could not be reworded at once, with a date. With `MESH_CATALOG_DIR` set it compares the catalogue's copy [`words-allowed.md`](words-allowed.md) names a document that could not be reworded at once, with a date. With `MESH_CATALOG_DIR` set it compares the catalog's copy
of the tools' retired words (`retired-words`) with the glossary. of the tools' retired words (`retired-words`) with the glossary.
It also fails on a **daemon's line** (ADR 0272, It also fails on a **daemon's line** (ADR 0272,
its insight of 2026-10-09): the operator types one command, `nox`, and a document names a `nox` line, never the its insight of 2026-10-09): the operator types one command, `nox`, and a document names a `nox` line, never the
controller's or the node-engine's binary followed by one of its commands. The lines are the check's own list controller's or the node-engine's binary followed by one of its commands. The lines are the check's own list
(`DAEMON_LINES`); a command that is a line only with an argument after it is matched as a line is written. Looked (`daemonLines` in the words test); a command that is a line only with an argument after it is matched as a line is written. Looked
for in the same documents, from 2026-10-09 for issues, in the document's own words — code blocks and for in the same documents, from 2026-10-09 for issues, in the document's own words — code blocks and
spans included, quotations and block quotes taken out. With `MESH_CATALOG_DIR` set it compares the catalogue's copy spans included, quotations and block quotes taken out. With `MESH_CATALOG_DIR` set it compares the catalog's copy
(`retired-lines`) with the list. (`retired-lines`) with the list.
It failed on something real before it passed: 581 uses of retired words in 63 documents on its first run, besides the glossary itself and research 034 — It failed on something real before it passed: 581 uses of retired words in 63 documents on its first run, besides the glossary itself and research 034 —
-173
View File
@@ -1,173 +0,0 @@
#!/usr/bin/env python3
"""The development cycle, checked.
The knowledge flow (00-META/process/00-overview.md) says work moves idea -> research ->
decision -> to-be design -> code, and symptom -> issue -> diagnosis -> fix. Those are rules,
and a rule states how it is checked (AGENTS.md) -- this is how. Everything here reads only
frontmatter, because a status this repository holds lives in frontmatter and nowhere else.
What is enforced:
design every 03-DESIGN doc parses, carries `layer:` matching its directory, and a
known `status:`. A TO-BE doc names at least one decision (`decisions:`) -- no
design without a decision -- and once `in-progress` or `implemented` it names
its owning code (`code:`) -- no development without a design that says where.
issues every report has frontmatter, and no two records share a number -- the number is
how a record is cited. An issue's status, owner, fix and replay live in the issue
tracker since the import of slice 3 (ADR 0275, to-be 53), where `issue-tracker.move`
refuses at the write what this check refused at merge until then: a status outside
the five, `located` without an owner, `resolved` without a fix, a core issue
resolved without its replay (ADR 0237), `wontfix` without a reason. A report
carries none of those keys, only the day it was `opened:` -- the tracker holds
them, and two places holding one fact drift. The keys the reports opened before
the import carried were removed once the import held them; the commit it read
is on every imported issue.
The decision records and research efforts live in the knowledge base (ADR 0290, issue 426), and the
rules this check held over them are the knowledge base's now: a research effort's status and what it
became, and a decision's duplicate number, are refused by `knowledge-base.propose`; an accepted decision
cited by nothing is the knowledge base's whole-set rule `cited`; a deferral that names no issue is its
rule `deferral`. README.md beside this file says where each went.
Deliberately NOT enforced: as-is docs need no decisions (they describe what exists, not what
was decided).
python3 00-META/checks/cycle.py
"""
import glob
import os
import re
import sys
ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", ".."))
DESIGN_STATUSES = {"proposed", "designed", "in-progress", "implemented", "abandoned"}
# The keys an issue report carried while its status lived here. The issue tracker holds them now (ADR 0275,
# slice 3 of to-be 53), and `issue-tracker.move` enforces the cycle's rules on them at the write. A report
# carrying one of them is a status written in two places, whenever it was opened.
TRACKER_KEYS = ("status", "located-in", "fixed-by", "amended-design", "replay", "replay-none", "resolved")
def rel(path):
return os.path.relpath(path, ROOT)
def frontmatter(path):
"""The YAML block between the first two --- lines, as {key: raw-value-string}.
Minimal on purpose, like records.py: enough for the fields these checks read. A list
value (block or inline) is joined into its items; a scalar stays a string.
"""
text = open(path, encoding="utf-8").read()
m = re.match(r"^---\n(.*?)\n---", text, re.S)
if not m:
return None
front, out, key = m.group(1), {}, None
for line in front.split("\n"):
item = re.match(r"^\s+-\s*(.+?)\s*$", line)
if item and key:
out[key].append(item.group(1))
continue
kv = re.match(r"^([A-Za-z-]+):\s*(.*)$", line)
if not kv:
continue
key, value = kv.group(1), kv.group(2).strip()
if value.startswith("[") and value.endswith("]"):
out[key] = [v.strip() for v in value[1:-1].split(",") if v.strip()]
elif value == "":
out[key] = [] # a block list may follow; stays [] if nothing does
else:
out[key] = value
return out
def listy(front, key):
v = front.get(key)
if v is None:
return []
return v if isinstance(v, list) else ([v] if str(v).strip() else [])
def main():
failures = []
def bad(path, why):
failures.append(" %s: %s" % (rel(path), why))
# ---- design ------------------------------------------------------------------------
for layer, name in (("00-as-is", "as-is"), ("01-to-be", "to-be")):
for path in sorted(glob.glob(os.path.join(ROOT, "03-DESIGN", layer, "*.md"))):
if os.path.basename(path) == "README.md":
continue
front = frontmatter(path)
if front is None:
bad(path, "no frontmatter")
continue
if front.get("layer") != name:
bad(path, "layer is %r; this directory is %s" % (front.get("layer"), name))
status = front.get("status")
if status not in DESIGN_STATUSES:
bad(path, "status %r is not one of %s" % (status, sorted(DESIGN_STATUSES)))
if name == "to-be":
if not listy(front, "decisions"):
bad(path, "names no decisions -- no design without a decision")
if status in ("in-progress", "implemented") and not listy(front, "code"):
bad(path, "status %s but code: names no owner -- no development "
"without a design that says where" % status)
# ---- issues ------------------------------------------------------------------------
# Two records may not share a number. Numbers are taken as "next free after main", and work
# sits on unmerged branches for days -- so two people reading the same main allocate the same
# number, and nothing said so. It happened twice in one evening between two machines, and the
# second collision landed on main with all three checks passing (issue 155). An issue number is
# how every other record cites this one; two records answering to it means a pointer that
# resolves to whichever the reader happened to open.
seen = {}
for folder in sorted(glob.glob(os.path.join(ROOT, "04-ISSUES", "*", ""))):
name = os.path.basename(os.path.normpath(folder))
number = name.split("-", 1)[0]
if not number.isdigit():
continue
if number in seen:
bad(os.path.join("04-ISSUES", name),
"is numbered %s, and so is %s -- an issue number is how it is cited, and two "
"records answering to one means a citation that resolves to whichever the reader "
"opened. Take the next free number across main AND every open pull request"
% (number, seen[number]))
else:
seen[number] = name
# Since the import of slice 3 (ADR 0275), an issue's status, owner, fix and replay live in the issue
# tracker, and its `move` refuses at the write what this check refused at merge. A report carries none
# of those keys.
for path in sorted(glob.glob(os.path.join(ROOT, "04-ISSUES", "*", "00-report.md"))):
front = frontmatter(path)
if front is None:
bad(path, "no frontmatter")
continue
opened = str(front.get("opened") or "")
if not opened:
bad(path, "opened: says nothing -- the day the issue was opened, YYYY-MM-DD")
continue
carried = [key for key in TRACKER_KEYS if key in front]
if carried:
bad(path, "carries %s, but an issue's status, owner, fix and replay live in the issue tracker "
"(ADR 0275): move it with `issue-tracker.move`, and keep no copy here"
% ", ".join("`%s:`" % key for key in carried))
checked = (
len(glob.glob(os.path.join(ROOT, "03-DESIGN", "0*", "*.md")))
+ len(glob.glob(os.path.join(ROOT, "04-ISSUES", "*", "00-report.md")))
)
if failures:
print("cycle: %d document(s) break the development cycle:" % len(failures))
print("\n".join(failures))
return 1
print("cycle: %d documents checked, the chain holds" % checked)
return 0
if __name__ == "__main__":
sys.exit(main())
+169
View File
@@ -0,0 +1,169 @@
package checks
import (
"fmt"
"io/fs"
"path"
"sort"
"strings"
"testing"
"testing/fstest"
)
// The development cycle, checked (cycle.py until ADR 0527; ADR 0080). Everything here reads frontmatter only,
// because a status this repository holds lives in frontmatter and nowhere else. The decision records and research
// efforts live in the knowledge base (ADR 0290), whose rules judge them.
var designStatuses = map[string]bool{"proposed": true, "designed": true, "in-progress": true, "implemented": true, "abandoned": true}
// The keys an issue report carried while its status lived here. The issue tracker holds them now (ADR 0275), and
// `issue-tracker.move` enforces the cycle's rules at the write. A report carrying one is a status in two places.
var trackerKeys = []string{"status", "located-in", "fixed-by", "amended-design", "replay", "replay-none", "resolved"}
// checkDesigns: every design parses, carries `layer:` matching its folder and a known `status:`; a to-be design
// names a decision, and once in-progress or implemented names its owning code. As-is designs need no decisions.
func checkDesigns(fsys fs.FS) []string {
var out []string
for _, layer := range []struct{ folder, name string }{{"00-as-is", "as-is"}, {"01-to-be", "to-be"}} {
entries, _ := fs.ReadDir(fsys, "03-DESIGN/"+layer.folder)
for _, e := range entries {
if e.IsDir() || !strings.HasSuffix(e.Name(), ".md") || e.Name() == "README.md" {
continue
}
p := path.Join("03-DESIGN", layer.folder, e.Name())
front := frontmatter(readFile(fsys, p))
if front == nil {
out = append(out, "cycle: "+p+": no frontmatter")
continue
}
if front.scalar("layer") != layer.name {
out = append(out, fmt.Sprintf("cycle: %s: layer is %q; this directory is %s", p, front.scalar("layer"), layer.name))
}
status := front.scalar("status")
if !designStatuses[status] {
var known []string
for s := range designStatuses {
known = append(known, s)
}
sort.Strings(known)
out = append(out, fmt.Sprintf("cycle: %s: status %q is not one of %v", p, status, known))
}
if layer.name == "to-be" {
if len(front.list("decisions")) == 0 {
out = append(out, "cycle: "+p+": names no decisions -- no design without a decision")
}
if (status == "in-progress" || status == "implemented") && len(front.list("code")) == 0 {
out = append(out, fmt.Sprintf("cycle: %s: status %s but code: names no owner -- no development without a design that says where", p, status))
}
}
}
}
return out
}
// checkIssues: no two issue folders share a number (issue 155: the number is how a record is cited), every report
// has frontmatter with `opened:`, and none carries a key the tracker holds.
func checkIssues(fsys fs.FS) []string {
var out []string
entries, _ := fs.ReadDir(fsys, "04-ISSUES")
seen := map[string]string{}
for _, e := range entries {
if !e.IsDir() {
continue
}
number := strings.SplitN(e.Name(), "-", 2)[0]
if number == "" || strings.Trim(number, "0123456789") != "" {
continue
}
if first, ok := seen[number]; ok {
out = append(out, fmt.Sprintf("cycle: 04-ISSUES/%s: is numbered %s, and so is %s -- an issue number is how it is cited, and two records "+
"answering to one means a citation that resolves to whichever the reader opened. Take the next free number across main AND every open pull request", e.Name(), number, first))
} else {
seen[number] = e.Name()
}
}
for _, e := range entries {
p := path.Join("04-ISSUES", e.Name(), "00-report.md")
if !e.IsDir() {
continue
}
if _, err := fs.Stat(fsys, p); err != nil {
continue
}
front := frontmatter(readFile(fsys, p))
if front == nil {
out = append(out, "cycle: "+p+": no frontmatter")
continue
}
if front.scalar("opened") == "" {
out = append(out, "cycle: "+p+": opened: says nothing -- the day the issue was opened, YYYY-MM-DD")
continue
}
var carried []string
for _, k := range trackerKeys {
if _, ok := front[k]; ok {
carried = append(carried, "`"+k+":`")
}
}
if len(carried) > 0 {
out = append(out, fmt.Sprintf("cycle: %s: carries %s, but an issue's status, owner, fix and replay live in the issue tracker (ADR 0275): "+
"move it with `issue-tracker.move`, and keep no copy here", p, strings.Join(carried, ", ")))
}
}
return out
}
func TestCycleDesignsHoldTheChain(t *testing.T) { expectNone(t, checkDesigns(rootFS())) }
func TestCycleIssuesHoldTheChain(t *testing.T) { expectNone(t, checkIssues(rootFS())) }
func design(layer, status, extra string) *fstest.MapFile {
return doc("---\nlayer: " + layer + "\nstatus: " + status + "\n" + extra + "---\nbody\n")
}
func TestCycleDesignsCatchesEachBreak(t *testing.T) {
broken := fstest.MapFS{
"03-DESIGN/00-as-is/a-nofront.md": doc("no frontmatter"),
"03-DESIGN/00-as-is/b-wronglayer.md": design("to-be", "implemented", ""),
"03-DESIGN/00-as-is/c-badstatus.md": design("as-is", "done", ""),
"03-DESIGN/01-to-be/d-nodecision.md": design("to-be", "designed", ""),
"03-DESIGN/01-to-be/e-nocode.md": design("to-be", "in-progress", "decisions: [ADR 0001]\n"),
"03-DESIGN/01-to-be/README.md": doc("not a design"),
}
got := checkDesigns(broken)
expectProblem(t, got, "a-nofront.md: no frontmatter")
expectProblem(t, got, `b-wronglayer.md: layer is "to-be"; this directory is as-is`)
expectProblem(t, got, `c-badstatus.md: status "done" is not one of`)
expectProblem(t, got, "d-nodecision.md: names no decisions")
expectProblem(t, got, "e-nocode.md: status in-progress but code: names no owner")
if len(got) != 5 {
t.Errorf("exactly the five breaks, as-is needs no decisions and a README is no design: %q", got)
}
}
func TestCycleDesignsAcceptsASoundPair(t *testing.T) {
sound := fstest.MapFS{
"03-DESIGN/00-as-is/a.md": design("as-is", "implemented", ""),
"03-DESIGN/01-to-be/b.md": design("to-be", "in-progress", "decisions: [ADR 0001]\ncode: [mesh-sdk]\n"),
}
expectNone(t, checkDesigns(sound))
}
func TestCycleIssuesCatchesEachBreak(t *testing.T) {
report := func(front string) *fstest.MapFile { return doc("---\n" + front + "---\nbody\n") }
broken := fstest.MapFS{
"04-ISSUES/100-a/00-report.md": report("opened: 2026-10-01\n"),
"04-ISSUES/100-b/00-report.md": report("opened: 2026-10-01\n"),
"04-ISSUES/101-noopened/00-report.md": report("title: x\n"),
"04-ISSUES/102-tracker/00-report.md": report("opened: 2026-10-01\nstatus: open\nreplay: none\n"),
"04-ISSUES/103-nofront/00-report.md": doc("plain"),
"04-ISSUES/notes/readme.md": doc("not an issue"),
}
got := checkIssues(broken)
expectProblem(t, got, "04-ISSUES/100-b: is numbered 100, and so is 100-a")
expectProblem(t, got, "101-noopened/00-report.md: opened: says nothing")
expectProblem(t, got, "102-tracker/00-report.md: carries `status:`, `replay:`")
expectProblem(t, got, "103-nofront/00-report.md: no frontmatter")
if len(got) != 4 {
t.Errorf("exactly the four breaks: %q", got)
}
}
-74
View File
@@ -1,74 +0,0 @@
#!/usr/bin/env python3
"""The reading order of the decision records: its topics are written here, its records are not.
A number identifies a record and never changes, so the reading order is not the order of the numbers.
It is the six topics `02-DECISIONS/README.md` writes down, in order, and each record's own topic. The
records live in the knowledge base (ADR 0290, issue 426), which reads these lines through the records
module and refuses a record whose topic is not one of them; the list of records under a topic is its
answer to `knowledge-base.holds` with that topic, never stored here (ADR 0248, hq issue 297).
python3 00-META/checks/index.py check
It fails when the README's reading order is missing or names a topic twice, and when the README stores
a list of records again — the index markers, or a line of a generated list. The last one is what keeps
the conflict of issue 297 from coming back. Whether every record's topic is one of the six is the
knowledge base's rule `topic`, judged when a record is proposed.
"""
import io
import re
import sys
README = "02-DECISIONS/README.md"
# A topic in the README's reading order, e.g.
# 1. **What the mesh is** — `topic: the mesh`. What it is for, ...
# The knowledge base (mesh-knowledge, reference.go) reads the same line, so the form is a contract.
TOPIC_LINE = re.compile(r"^\d+\.\s+\*\*(?P<label>[^*]+)\*\*\s+—\s+`topic:\s*(?P<topic>[^`]+)`", re.M)
# What a stored list looks like: the old markers, or a line of the list index.py used to print.
STORED = [
(re.compile(r"<!--\s*index:(start|end)\s*-->"), "an index marker"),
(re.compile(r"^- \*\*\d{4}\*\* — \[", re.M), "a line of a generated list"),
]
def reading_order(text=None):
"""The topics, in order, as (topic, label) pairs, from the README."""
if text is None:
text = io.open(README, encoding='utf-8').read()
return [(m.group("topic").strip(), m.group("label").strip()) for m in TOPIC_LINE.finditer(text)]
def main():
if "--print" in sys.argv:
print("index: the records live in the knowledge base (ADR 0290). Read a topic's with "
"`knowledge-base.holds` and that topic, through the mesh MCP server.")
return 2
text = io.open(README, encoding='utf-8').read()
topics = reading_order(text)
problems = []
if not topics:
problems.append("%s writes no reading order: no line of the form "
"'1. **Label** — `topic: name`. ...'; the knowledge base refuses every "
"record without it" % README)
names = [t for t, _ in topics]
if len(set(names)) != len(names):
problems.append("%s names a topic twice in its reading order" % README)
for pattern, what in STORED:
if pattern.search(text):
problems.append("%s stores a list of records (%s). The records live in the knowledge base, "
"and a topic's list is `knowledge-base.holds` with that topic" % (README, what))
for p in problems:
print("index: " + p)
if problems:
return 1
print("index: %d topics in the reading order, no stored list" % len(topics))
return 0
if __name__ == "__main__":
sys.exit(main())
+93
View File
@@ -0,0 +1,93 @@
package checks
import (
"fmt"
"io/fs"
"regexp"
"strings"
"testing"
"testing/fstest"
)
// The reading order of the decision records (index.py until ADR 0527): its topics are written in
// 02-DECISIONS/README.md, its records are not. The knowledge base reads these lines and refuses a record whose
// topic is not one of them, so the form is a contract; a topic's records are `knowledge-base.holds` with that
// topic, never stored here (ADR 0248, issue 297).
const readmeOfDecisions = "02-DECISIONS/README.md"
// A topic line: `1. **Label** — `topic: name`. ...`
var topicLineRx = regexp.MustCompile(`(?m)^\d+\.\s+\*\*([^*]+)\*\*\s+—\s+` + "`topic:\\s*([^`]+)`")
// What a stored list looks like: the old markers, or a line of the list index.py used to print.
var storedLists = []struct {
rx *regexp.Regexp
what string
}{
{regexp.MustCompile(`<!--\s*index:(start|end)\s*-->`), "an index marker"},
{regexp.MustCompile(`(?m)^- \*\*\d{4}\*\* — \[`), "a line of a generated list"},
}
func readingOrder(text string) []string {
var topics []string
for _, m := range topicLineRx.FindAllStringSubmatch(text, -1) {
topics = append(topics, strings.TrimSpace(m[2]))
}
return topics
}
func checkIndex(fsys fs.FS) []string {
text := readFile(fsys, readmeOfDecisions)
topics := readingOrder(text)
var out []string
if len(topics) == 0 {
out = append(out, "index: "+readmeOfDecisions+" writes no reading order: no line of the form '1. **Label** — `topic: name`. ...'; "+
"the knowledge base refuses every record without it")
}
seen := map[string]bool{}
for _, topic := range topics {
if seen[topic] {
out = append(out, fmt.Sprintf("index: %s names a topic twice in its reading order: %s", readmeOfDecisions, topic))
break
}
seen[topic] = true
}
for _, s := range storedLists {
if s.rx.MatchString(text) {
out = append(out, fmt.Sprintf("index: %s stores a list of records (%s). The records live in the knowledge base, and a topic's list is "+
"`knowledge-base.holds` with that topic", readmeOfDecisions, s.what))
}
}
return out
}
func TestIndexReadingOrderIsWrittenAndStoresNoList(t *testing.T) {
expectNone(t, checkIndex(rootFS()))
if n := len(readingOrder(readFile(rootFS(), readmeOfDecisions))); n == 0 {
t.Error("the reading order has no topic")
}
}
const sound = "1. **One** — `topic: one`. x\n2. **Two** — `topic: two`. y\n"
func TestIndexCatchesAMissingReadingOrder(t *testing.T) {
expectProblem(t, checkIndex(fstest.MapFS{readmeOfDecisions: doc("nothing written here\n")}), "writes no reading order")
}
func TestIndexCatchesADuplicateTopic(t *testing.T) {
broken := fstest.MapFS{readmeOfDecisions: doc(sound + "3. **Again** — `topic: one`. z\n")}
expectProblem(t, checkIndex(broken), "names a topic twice")
}
func TestIndexCatchesAStoredList(t *testing.T) {
for what, stored := range map[string]string{
"an index marker": "<!-- index:start -->\n",
"a line of a generated list": "- **0001** — [a title](x.md)\n",
} {
expectProblem(t, checkIndex(fstest.MapFS{readmeOfDecisions: doc(sound + stored)}), what)
}
}
func TestIndexAcceptsASoundReadme(t *testing.T) {
expectNone(t, checkIndex(fstest.MapFS{readmeOfDecisions: doc(sound)}))
}
+1
View File
@@ -0,0 +1 @@
These tests read the repository's documents (rootFS), outside this directory: mesh-build-go never keeps this package's verdict (issue 1935).
-227
View File
@@ -1,227 +0,0 @@
#!/usr/bin/env python3
"""Structural checks over HQ's own documents.
Every check here exists because the thing it checks for actually happened. See README.md
for which incident is behind which check. Run from the repository root:
python3 00-META/checks/records.py
Exits non-zero if anything fails, so it can be a gate rather than a report.
The decision records and research efforts live in the knowledge base (ADR 0290, issue 426), and the
rules that judged them here are the knowledge base's now: `propose` refuses a record that breaks one.
README.md beside this file says where each rule went. What stays here judges what this repository
still holds, and keeps the records from coming back.
"""
import os
import re
import sys
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
LINK = re.compile(r"\[[^\]]*\]\((?!https?:|mailto:)([^)]+)\)")
# The two folders the records left. Each keeps a README that points at the knowledge base, and
# 02-DECISIONS/README.md also writes the reading order's topics, which the knowledge base reads.
RECORD_FOLDERS = ("02-DECISIONS", "01-RESEARCH")
POINTERS = {"02-DECISIONS/README.md", "01-RESEARCH/README.md"}
# A decision record is named "ADR NNNN", four digits, and a research effort "research NNN", three.
DECISION_ENTRY = re.compile(r"^ADR \d{4}$")
ADR_MENTION = re.compile(r"\bADRs?\s+(\d+)\b")
RESEARCH_MENTION = re.compile(r"\b[Rr]esearch\s+(\d+)\b")
def markdown_files():
for base, dirs, files in os.walk(ROOT):
dirs[:] = [d for d in dirs if d not in (".git", ".claude")]
for name in sorted(files):
if name.endswith(".md"):
yield os.path.join(base, name)
def rel(path):
return os.path.relpath(path, ROOT)
def read(path):
with open(path, encoding="utf-8") as handle:
return handle.read()
def frontmatter(text):
"""Minimal frontmatter reader — enough for the fields these checks use.
Not a YAML parser on purpose: a dependency in a repository that has none, to read a few
scalar fields and one list, would cost more than it returns.
"""
if not text.startswith("---\n"):
return {}
end = text.find("\n---", 4)
if end == -1:
return {}
fields, key = {}, None
for line in text[4:end].split("\n"):
item = re.match(r"^\s+-\s+(.*)$", line)
if item and key:
fields.setdefault(key, []).append(item.group(1).strip())
continue
pair = re.match(r"^([A-Za-z_-]+):\s*(.*)$", line)
if pair:
key = pair.group(1)
value = pair.group(2).strip()
if value.startswith("[") and value.endswith("]"):
# inline list: `decisions: [a, b]`, `code: []`
inner = value[1:-1].strip()
fields[key] = [v.strip() for v in inner.split(",") if v.strip()]
elif value:
fields[key] = value
else:
fields[key] = []
return fields
def unquote(value):
value = value.strip()
if len(value) >= 2 and value[0] == value[-1] and value[0] in "\"'":
return value[1:-1]
return value
class Failures:
def __init__(self):
self.items = []
def add(self, check, location, message):
self.items.append((check, location, message))
def report(self):
if not self.items:
print("records: all checks passed")
return 0
by_check = {}
for check, location, message in self.items:
by_check.setdefault(check, []).append((location, message))
for check in sorted(by_check):
print(f"\n{check} — {len(by_check[check])} problem(s)")
for location, message in by_check[check]:
print(f" {location}\n {message}")
print(f"\nrecords: {len(self.items)} problem(s)")
return 1
def check_links(failures):
"""Every relative link resolves to something that exists."""
for path in markdown_files():
folder = os.path.dirname(path)
for number, line in enumerate(read(path).split("\n"), 1):
for target in LINK.findall(line):
target = target.split("#")[0].strip()
if not target:
continue
if not os.path.exists(os.path.normpath(os.path.join(folder, target))):
failures.add("links", f"{rel(path)}:{number}", f"link does not resolve: {target}")
def check_no_records(failures):
"""No decision record or research effort is written into this repository again (ADR 0290 rule 9).
The knowledge base is the only place a record is read; a file here would be a second copy that
drifts from it, and nothing would read it. The folders keep their pointer READMEs and nothing else.
"""
for folder in RECORD_FOLDERS:
base = os.path.join(ROOT, folder)
if not os.path.isdir(base):
failures.add("no-records", folder, "the folder is gone; it keeps a README that points at the "
"knowledge base (and, for 02-DECISIONS, the reading order the knowledge base reads)")
continue
for current, dirs, files in os.walk(base):
for name in sorted(files + [d + "/" for d in dirs]):
path = rel(os.path.join(current, name))
if path.rstrip("/") in POINTERS:
continue
failures.add(
"no-records",
path,
"a decision record or research effort lives in the knowledge base, never here: "
"write it with knowledge-base.reserve and knowledge-base.propose (playbooks 01 and 02)",
)
def check_decisions_entries(failures):
"""A design's `decisions:` names each record as "ADR NNNN".
A to-be design rests on the records it names, and the knowledge base answers for a record by its
number; an entry in another form is a citation nothing can follow.
"""
for path in markdown_files():
if not rel(path).startswith("03-DESIGN/"):
continue
for entry in frontmatter(read(path)).get("decisions", []) or []:
if not DECISION_ENTRY.match(unquote(entry)):
failures.add("decisions-form", rel(path),
f"decisions: entry {entry!r} is not 'ADR NNNN' (four digits)")
def check_mentions(failures):
"""In running text, a decision record is "ADR NNNN" (four digits) and a research effort "research NNN"
(three): the form the knowledge base and the issue tracker read a citation in. A code span is an example,
and is not read."""
for path in markdown_files():
text = read(path)
for number, line in enumerate(text.split("\n"), 1):
line = re.sub(r"`[^`]*`", "", line)
for m in ADR_MENTION.finditer(line):
if len(m.group(1)) != 4:
failures.add("mention-form", f"{rel(path)}:{number}",
f"{m.group(0)!r}: a decision record is named 'ADR NNNN', four digits")
for m in RESEARCH_MENTION.finditer(line):
if len(m.group(1)) != 3:
failures.add("mention-form", f"{rel(path)}:{number}",
f"{m.group(0)!r}: a research effort is named 'research NNN', three digits")
def check_status_against_code(failures):
"""A design document naming specific code may not still call itself `designed`.
**Naming a file is a claim that the file implements this**, so the two fields have to agree.
They drifted: ten to-be documents named working, lab-proven code — several with a *What was
built* or *Raised, and observed* section — while still saying nothing had been built.
Deliberately weak, and that is the point of it being mechanical. It cannot tell whether the
prose is true, only that a document has stopped claiming to be unbuilt once it points at
something. `code: [mesh-controller]` — a repository with no path — is a plan and stays
`designed`.
"""
for path in markdown_files():
if not rel(path).startswith("03-DESIGN/01-to-be/") or path.endswith("README.md"):
continue
front = frontmatter(read(path))
if front.get("status") != "designed":
continue
for entry in front.get("code") or []:
named = re.sub(r"\s*\(.*\)$", "", entry).strip().split(None, 1)
if len(named) > 1:
failures.add(
"status-vs-code",
rel(path),
f"`designed`, but names {named[1]!r} in {named[0]}. Naming a file claims "
f"it implements this — use `in-progress`, or `implemented` once it is "
f"defensible from that repository's main branch.",
)
break
def main():
failures = Failures()
check_links(failures)
check_no_records(failures)
check_decisions_entries(failures)
check_mentions(failures)
check_status_against_code(failures)
return failures.report()
if __name__ == "__main__":
sys.exit(main())
+206
View File
@@ -0,0 +1,206 @@
package checks
import (
"fmt"
"io/fs"
"path"
"regexp"
"strings"
"testing"
"testing/fstest"
)
// Structural checks over hq's own documents (records.py until ADR 0527). The decision records and research
// efforts live in the knowledge base (ADR 0290, issue 426); what stays here judges what this repository still
// holds, and keeps the records from coming back.
var (
linkRx = regexp.MustCompile(`\[[^\]]*\]\(([^)]+)\)`)
// A decision record is named "ADR NNNN", four digits, and a research effort "research NNN", three.
decisionEntryRx = regexp.MustCompile(`^ADR \d{4}$`)
adrMentionRx = regexp.MustCompile(`\bADRs?\s+(\d+)\b`)
researchMentionRx = regexp.MustCompile(`\b[Rr]esearch\s+(\d+)\b`)
codeSpanRx = regexp.MustCompile("`[^`]*`")
recordFolders = []string{"02-DECISIONS", "01-RESEARCH"}
recordFolderPoints = map[string]bool{"02-DECISIONS/README.md": true, "01-RESEARCH/README.md": true}
)
// checkLinks: every relative link resolves to something that exists.
func checkLinks(fsys fs.FS) []string {
var out []string
for _, p := range markdownFiles(fsys) {
for n, line := range strings.Split(readFile(fsys, p), "\n") {
for _, m := range linkRx.FindAllStringSubmatch(line, -1) {
target := m[1]
if strings.HasPrefix(target, "http:") || strings.HasPrefix(target, "https:") || strings.HasPrefix(target, "mailto:") {
continue
}
target = strings.TrimSpace(strings.SplitN(target, "#", 2)[0])
if target == "" {
continue
}
if _, err := fs.Stat(fsys, path.Join(path.Dir(p), target)); err != nil {
out = append(out, fmt.Sprintf("links: %s:%d: link does not resolve: %s", p, n+1, target))
}
}
}
}
return out
}
// checkNoRecords: the two folders the records left hold their pointer README and nothing else (ADR 0290 rule 9).
func checkNoRecords(fsys fs.FS) []string {
var out []string
for _, folder := range recordFolders {
if _, err := fs.Stat(fsys, folder); err != nil {
out = append(out, "no-records: "+folder+": the folder is gone; it keeps a README that points at the knowledge base")
continue
}
_ = fs.WalkDir(fsys, folder, func(p string, d fs.DirEntry, err error) error {
if err != nil || p == folder || recordFolderPoints[p] {
return nil
}
out = append(out, "no-records: "+p+": a decision record or research effort lives in the knowledge base, never here: "+
"write it with knowledge-base.reserve and knowledge-base.propose (playbooks 01 and 02)")
return nil
})
}
return out
}
// checkDecisionsEntries: a design's `decisions:` names each record as "ADR NNNN".
func checkDecisionsEntries(fsys fs.FS) []string {
var out []string
for _, p := range markdownFiles(fsys) {
if !strings.HasPrefix(p, "03-DESIGN/") {
continue
}
for _, e := range frontmatter(readFile(fsys, p)).list("decisions") {
if !decisionEntryRx.MatchString(unquote(e)) {
out = append(out, fmt.Sprintf("decisions-form: %s: decisions: entry %q is not 'ADR NNNN' (four digits)", p, e))
}
}
}
return out
}
// checkMentions: in running text a decision record is "ADR NNNN" and a research effort "research NNN"; a code
// span is an example and is not read.
func checkMentions(fsys fs.FS) []string {
var out []string
for _, p := range markdownFiles(fsys) {
for n, line := range strings.Split(readFile(fsys, p), "\n") {
line = codeSpanRx.ReplaceAllString(line, "")
for _, m := range adrMentionRx.FindAllStringSubmatch(line, -1) {
if len(m[1]) != 4 {
out = append(out, fmt.Sprintf("mention-form: %s:%d: %q: a decision record is named 'ADR NNNN', four digits", p, n+1, m[0]))
}
}
for _, m := range researchMentionRx.FindAllStringSubmatch(line, -1) {
if len(m[1]) != 3 {
out = append(out, fmt.Sprintf("mention-form: %s:%d: %q: a research effort is named 'research NNN', three digits", p, n+1, m[0]))
}
}
}
}
return out
}
var codeEntryRx = regexp.MustCompile(`\s*\(.*\)$`)
// checkStatusAgainstCode: a to-be design naming specific code (a path after the repository) may not still be
// `designed`. Deliberately weak: it cannot tell whether the prose is true, only that a document stopped claiming
// to be unbuilt once it points at something. `code: [mesh-controller]` is a plan and stays `designed`.
func checkStatusAgainstCode(fsys fs.FS) []string {
var out []string
for _, p := range markdownFiles(fsys) {
if !strings.HasPrefix(p, "03-DESIGN/01-to-be/") || strings.HasSuffix(p, "README.md") {
continue
}
front := frontmatter(readFile(fsys, p))
if front.scalar("status") != "designed" {
continue
}
for _, e := range front.list("code") {
named := strings.SplitN(strings.TrimSpace(codeEntryRx.ReplaceAllString(e, "")), " ", 2)
if len(named) > 1 {
out = append(out, fmt.Sprintf("status-vs-code: %s: `designed`, but names %q in %s. Naming a file claims it implements this "+
"- use `in-progress`, or `implemented` once it is defensible from that repository's main branch.", p, strings.TrimSpace(named[1]), named[0]))
break
}
}
}
return out
}
func doc(s string) *fstest.MapFile { return &fstest.MapFile{Data: []byte(s)} }
// The real tree.
func TestRecordsLinksResolve(t *testing.T) { expectNone(t, checkLinks(rootFS())) }
func TestRecordsFoldersHoldOnlyPointers(t *testing.T) { expectNone(t, checkNoRecords(rootFS())) }
func TestRecordsDecisionsEntriesForm(t *testing.T) { expectNone(t, checkDecisionsEntries(rootFS())) }
func TestRecordsMentionForm(t *testing.T) { expectNone(t, checkMentions(rootFS())) }
func TestRecordsStatusAgainstCode(t *testing.T) { expectNone(t, checkStatusAgainstCode(rootFS())) }
// The checks themselves, each against a broken fixture, and against a sound one.
func TestRecordsLinksCatchesABrokenLink(t *testing.T) {
broken := fstest.MapFS{
"a/one.md": doc("see [two](two.md) and [gone](gone.md#x) and [web](https://example.com/x)\n"),
"a/two.md": doc("[up](../a/one.md#top) [anchor](#here)\n"),
}
got := checkLinks(broken)
expectProblem(t, got, "a/one.md:1: link does not resolve: gone.md")
if len(got) != 1 {
t.Errorf("only the broken link is a problem, got %q", got)
}
}
func TestRecordsNoRecordsCatchesARecordPutBack(t *testing.T) {
broken := fstest.MapFS{
"02-DECISIONS/README.md": doc("pointer"),
"02-DECISIONS/0001-x.md": doc("a record"),
"01-RESEARCH/README.md": doc("pointer"),
"01-RESEARCH/045/00-x.md": doc("an effort"),
}
got := checkNoRecords(broken)
expectProblem(t, got, "02-DECISIONS/0001-x.md")
expectProblem(t, got, "01-RESEARCH/045")
expectProblem(t, checkNoRecords(fstest.MapFS{"02-DECISIONS/README.md": doc("p")}), "01-RESEARCH: the folder is gone")
}
func TestRecordsDecisionsFormCatchesAShortEntry(t *testing.T) {
broken := fstest.MapFS{
"03-DESIGN/01-to-be/1.md": doc("---\nlayer: to-be\ndecisions:\n - ADR 0290\n - ADR 290\n---\n"),
"00-META/x.md": doc("---\ndecisions: [ADR 5]\n---\n"),
}
got := checkDecisionsEntries(broken)
expectProblem(t, got, `entry "ADR 290"`)
if len(got) != 1 {
t.Errorf("only a design's entries are read, and ADR 0290 is sound: %q", got)
}
}
func TestRecordsMentionFormCatchesShortNumbers(t *testing.T) {
broken := fstest.MapFS{"x.md": doc("fine ADR 0290 and research 045\nbad ADR 12 and research 45\nexample `ADR 7` and `research 9`\n")}
got := checkMentions(broken)
expectProblem(t, got, "x.md:2: \"ADR 12\"")
expectProblem(t, got, "x.md:2: \"research 45\"")
if len(got) != 2 {
t.Errorf("code spans and sound forms are not problems: %q", got)
}
}
func TestRecordsStatusAgainstCodeCatchesADesignedDesignNamingAFile(t *testing.T) {
broken := fstest.MapFS{
"03-DESIGN/01-to-be/9.md": doc("---\nstatus: designed\ncode:\n - mesh-controller internal/x.go (the thing)\n---\n"),
"03-DESIGN/01-to-be/8.md": doc("---\nstatus: designed\ncode: [mesh-controller]\n---\n"),
"03-DESIGN/01-to-be/7.md": doc("---\nstatus: in-progress\ncode: [mesh-controller internal/x.go]\n---\n"),
}
got := checkStatusAgainstCode(broken)
expectProblem(t, got, "03-DESIGN/01-to-be/9.md: `designed`, but names \"internal/x.go\" in mesh-controller")
if len(got) != 1 {
t.Errorf("a plan without a path and an in-progress design are sound: %q", got)
}
}
+39
View File
@@ -0,0 +1,39 @@
package checks
import (
"reflect"
"testing"
"testing/fstest"
)
func TestMarkdownFilesSkipsToolFolders(t *testing.T) {
fsys := fstest.MapFS{
"a.md": {Data: []byte("x")},
"b/c.md": {Data: []byte("x")},
"b/c.txt": {Data: []byte("x")},
".git/d.md": {Data: []byte("x")},
".claude/e.md": {Data: []byte("x")},
".work/f/g.md": {Data: []byte("x")},
}
got := markdownFiles(fsys)
want := []string{"a.md", "b/c.md"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("markdownFiles = %q, want %q", got, want)
}
}
func TestFrontmatterReadsScalarsAndLists(t *testing.T) {
f := frontmatter("---\nlayer: to-be\ndecisions:\n - ADR 0001\n - ADR 0002\ncode: []\ncodes: [a, b]\n---\nbody\n")
if f.scalar("layer") != "to-be" {
t.Errorf("layer = %q", f.scalar("layer"))
}
if !reflect.DeepEqual(f.list("decisions"), []string{"ADR 0001", "ADR 0002"}) {
t.Errorf("decisions = %q", f.list("decisions"))
}
if len(f.list("code")) != 0 || !reflect.DeepEqual(f.list("codes"), []string{"a", "b"}) {
t.Errorf("code = %q, codes = %q", f.list("code"), f.list("codes"))
}
if frontmatter("no frontmatter") != nil || frontmatter("---\nunclosed") != nil {
t.Error("a document without a closed block has no frontmatter")
}
}
+150
View File
@@ -0,0 +1,150 @@
// Package checks holds hq's four checks as Go tests (ADR 0527): records, index, cycle and words.
//
// The package has tests only. Each check is a function over an fs.FS that returns its problems, so
// the test of the real tree reads the repository's root, and the test of the check itself reads a
// deliberately broken tree built in memory and must see the problem named (every check here failed on
// a broken fixture before it passed). The shared reading of Markdown and frontmatter is in this file.
package checks
import (
"io/fs"
"os"
"regexp"
"sort"
"strings"
"testing"
)
// rootFS is hq's tree: the checks run from 00-META/checks, two folders under the root.
func rootFS() fs.FS { return os.DirFS("../..") }
// skipDirs are folders no check reads: git's, the agent tooling's and a session's scratch folder.
var skipDirs = map[string]bool{".git": true, ".claude": true, ".work": true}
// markdownFiles lists every Markdown file of the tree, sorted, in slash paths from the root.
func markdownFiles(fsys fs.FS) []string {
var out []string
_ = fs.WalkDir(fsys, ".", func(p string, d fs.DirEntry, err error) error {
if err != nil {
return nil
}
if d.IsDir() {
if p != "." && skipDirs[d.Name()] {
return fs.SkipDir
}
return nil
}
if strings.HasSuffix(p, ".md") {
out = append(out, p)
}
return nil
})
sort.Strings(out)
return out
}
func readFile(fsys fs.FS, p string) string {
b, err := fs.ReadFile(fsys, p)
if err != nil {
return ""
}
return string(b)
}
// Front is a document's frontmatter: a scalar field holds a string, a list field a []string.
type Front map[string]any
func (f Front) scalar(key string) string {
s, _ := f[key].(string)
return s
}
func (f Front) list(key string) []string {
switch v := f[key].(type) {
case []string:
return v
case string:
if strings.TrimSpace(v) != "" {
return []string{v}
}
}
return nil
}
var (
fmItem = regexp.MustCompile(`^\s+-\s*(.+?)\s*$`)
fmPair = regexp.MustCompile(`^([A-Za-z_-]+):\s*(.*)$`)
)
// frontmatter reads the block between the first two --- lines. It is not a YAML parser on purpose: a
// dependency to read a few scalar fields and one list would cost more than it returns. It returns nil
// when the document has no frontmatter.
func frontmatter(text string) Front {
if !strings.HasPrefix(text, "---\n") {
return nil
}
end := strings.Index(text[4:], "\n---")
if end == -1 {
return nil
}
out := Front{}
key := ""
for _, line := range strings.Split(text[4:4+end], "\n") {
if m := fmItem.FindStringSubmatch(line); m != nil && key != "" {
list, _ := out[key].([]string)
out[key] = append(list, m[1])
continue
}
m := fmPair.FindStringSubmatch(line)
if m == nil {
continue
}
key = m[1]
value := strings.TrimSpace(m[2])
switch {
case strings.HasPrefix(value, "[") && strings.HasSuffix(value, "]"):
list := []string{}
for _, v := range strings.Split(value[1:len(value)-1], ",") {
if v = strings.TrimSpace(v); v != "" {
list = append(list, v)
}
}
out[key] = list
case value == "":
out[key] = []string{}
default:
out[key] = value
}
}
return out
}
func unquote(v string) string {
v = strings.TrimSpace(v)
if len(v) >= 2 && v[0] == v[len(v)-1] && (v[0] == '"' || v[0] == '\'') {
return v[1 : len(v)-1]
}
return v
}
// lineOf is the 1-based line of byte offset i in text.
func lineOf(text string, i int) int { return strings.Count(text[:i], "\n") + 1 }
// expectNone fails the test with every problem a check found in the real tree.
func expectNone(t *testing.T, problems []string) {
t.Helper()
for _, p := range problems {
t.Error(p)
}
}
// expectProblem fails unless some problem contains want: the broken fixture must be caught, by name.
func expectProblem(t *testing.T, problems []string, want string) {
t.Helper()
for _, p := range problems {
if strings.Contains(p, want) {
return
}
}
t.Errorf("no problem mentions %q; got %q", want, problems)
}
+14 -2
View File
@@ -1,6 +1,6 @@
# Words allowed — documents `words.py` does not hold to the glossary's retired words # Words allowed — documents the words test does not hold to the glossary's retired words
Read by [`words.py`](words.py) (ADR 0244). Read by the words test (`words_test.go`) (ADR 0244).
One row per document. *Until* is the date by which it is reworded — past it, the row fails like the words One row per document. *Until* is the date by which it is reworded — past it, the row fails like the words
themselves. A row whose document no longer uses a retired word fails too, so this list only shrinks. The themselves. A row whose document no longer uses a retired word fails too, so this list only shrinks. The
research efforts this list kept as written (research 034 and 042) live in the knowledge base now (ADR 0290), research efforts this list kept as written (research 034 and 042) live in the knowledge base now (ADR 0290),
@@ -8,3 +8,15 @@ which keeps a record's words as it was imported.
| Document | Until | Why | | Document | Until | Why |
|---|---|---| |---|---|---|
| `04-ISSUES/285-the-merge-gate-compared-broken-to-broken-and-passed/00-report.md` | 2027-04-30 | history: the issue's narrative keeps the word of its day, "merge gate" (issue 1303); it is not rewritten |
| `04-ISSUES/300-a-new-module-is-not-built-at-its-merge/00-report.md` | 2027-04-30 | history: the issue's narrative keeps the word of its day, "merge gate" (issue 1303); it is not rewritten |
| `04-ISSUES/302-the-labs-go-replays-were-never-checked-before-merge/01-diagnosis.md` | 2027-04-30 | history: the issue's narrative keeps the word of its day, "merge gate" (issue 1303); it is not rewritten |
| `04-ISSUES/305-a-recheck-left-the-old-verdict-standing/00-report.md` | 2027-04-30 | history: the issue's narrative keeps the word of its day, "merge gate" (issue 1303); it is not rewritten |
| `04-ISSUES/336-a-send-held-for-the-bus-step-raises-nothing/00-report.md` | 2027-04-30 | history: the issue's narrative keeps the word of its day, "merge gate" (issue 1303); it is not rewritten |
| `04-ISSUES/336-a-send-held-for-the-bus-step-raises-nothing/01-diagnosis.md` | 2027-04-30 | history: the issue's narrative keeps the word of its day, "merge gate" (issue 1303); it is not rewritten |
| `04-ISSUES/338-a-module-built-from-a-shared-repository-moves-on-every-merge-to-it/01-diagnosis.md` | 2027-04-30 | history: the issue's narrative keeps the word of its day, "merge gate" (issue 1303); it is not rewritten |
| `04-ISSUES/341-a-merge-check-judges-the-head-not-what-merging-it-makes/01-diagnosis.md` | 2027-04-30 | history: the issue's narrative keeps the word of its day, "merge gate" (issue 1303); it is not rewritten |
| `04-ISSUES/353-the-controller-asked-before-the-bus-held-its-grant/00-report.md` | 2027-04-30 | history: the issue's narrative keeps the word of its day, "merge gate" (issue 1303); it is not rewritten |
| `04-ISSUES/354-a-decided-rename-was-never-scheduled-and-the-operator-believed-it-live/01-diagnosis.md` | 2027-04-30 | history: the issue's narrative keeps the word of its day, "merge gate" (issue 1303); it is not rewritten |
| `04-ISSUES/362-two-merges-seconds-apart-left-their-delivery-waiting-for-a-person/01-diagnosis.md` | 2027-04-30 | history: the issue's narrative keeps the word of its day, "merge gate" (issue 1303); it is not rewritten |
| `04-ISSUES/364-the-gate-refuses-a-module-that-uses-a-seat-another-module-declares/00-report.md` | 2027-04-30 | history: the issue's narrative keeps the word of its day, "merge gate" (issue 1303); it is not rewritten |
-377
View File
@@ -1,377 +0,0 @@
#!/usr/bin/env python3
"""One word per thing, checked (ADR 0244).
The glossary (00-META/glossary.md) is the authority on the mesh's words. It names the words it
retired on one fixed kind of line, so a program can read them:
*Not:* ~~control plane~~, ~~overlay~~ (hq)
A struck word with no scope is retired everywhere; `(hq)` retires it in this repository's prose only;
`(tools)` in the descriptions of the mesh's tools only. And it names code that still carries an old
name until that code is renamed:
*Identifier until renamed:* `mesh-host` — the repository, binary and service unit
An identifier is allowed inside a code span and nowhere else.
What is enforced:
retired no retired word of scope `hq` (or none), and no identifier, in running prose of a
checked document. Running prose is what is left once code blocks, code spans, block
quotes, text in quotation marks, struck-through text, link targets, HTML comments and
frontmatter are taken out: a quotation keeps the words it quotes, a link target is a file
name, and an identifier lives in a code span. A word's plural is the word: "alerts" fails
as "alert" does.
unique no head word (a bolded word opening a glossary entry) heads two entries, and no head
word is also struck through on a *Not:* line.
tools list with `--list tools` it prints the words retired in the tools' descriptions, which is the
copy the catalogue's own check keeps (mesh-catalog `retired-words`). With
MESH_CATALOG_DIR set to a checkout of the catalogue it also fails when that copy differs.
lines no daemon's line in a checked document (ADR 0272, its insight of 2026-10-09). The operator
types one command, `nox` (`mesh-cli`); the node-engine (`nox-mesh-host`) and the
controller (`mesh-controller`) are daemons, and a person never types their names — so a
document names a `nox` line, never `mesh-controller push` or `mesh-host hand-over`. The
lines are DAEMON_LINES below: each daemon's commands, and which of them are a line only
with an argument after them ("the mesh-controller module" is prose; `mesh-controller
module check …` is a line). A line is looked for in the document's own words — code
blocks and code spans included, since that is where a line to type is written — with
quotations, block quotes, link targets and comments taken out. `--list lines` prints the
list, which the catalogue's own check keeps a copy of (mesh-catalog `retired-lines`); with
MESH_CATALOG_DIR set it also fails when that copy differs. The node-engine's install-time
subcommands (`nox-mesh-host enrol`, `key`, `reconcile`, …) are not lines here: they are
typed at a machine no mesh reaches yet, and the engine's usage says so.
Checked documents: 00-META/, 03-DESIGN/ (both layers), AGENTS.md and README.md, always; an issue opened
on or after FROM (on or after LINES_FROM for the lines). The decision records and research efforts live in
the knowledge base (ADR 0290, issue 426), which holds a record proposed there to the same words through its
rules `retired-word` and `daemon-line`, read from this glossary.
A document the check fails on that cannot be reworded in the change that found it is named, with a date
and a reason, in words-allowed.md beside this file. An entry past its date fails like the word itself, and
an entry for a document that no longer needs it fails too.
python3 00-META/checks/words.py
python3 00-META/checks/words.py --list tools
python3 00-META/checks/words.py --list lines
"""
import datetime
import os
import re
import sys
ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", ".."))
GLOSSARY = "00-META/glossary.md"
ALLOWED = "00-META/checks/words-allowed.md"
# The day the rule began (ADR 0244): issues opened from it on are held to it.
FROM = "2026-10-07"
ALWAYS = ("00-META/", "03-DESIGN/", "AGENTS.md", "README.md")
# The daemons' lines (ADR 0272, its insight of 2026-10-09), and the day that rule began. "alone" is a
# command that is a line by itself; "with an argument" one that is a line only when written as one.
LINES_FROM = "2026-10-09"
DAEMON_LINES = {
"mesh-controller": {
"alone": ("adopt assign broker cancel cleanup collect conditions converge doctor durations hand-act "
"hand-acts healers identity kill licence migrate operator pause pin plan prepare push rebuild "
"rehearse replay resume retire rollout rotate secret settings status token unassign unpin "
"upgrade").split(),
"with an argument": ("artifacts ask builds bus collection data delivery images mirrors module node "
"overlay plans queue seat seats take version").split(),
},
"mesh-host": {
"alone": "apply bundle enrol enroll hand-over help inventory key overlay owned profile reconcile run version".split(),
"with an argument": [],
},
"nox-mesh-host": {"alone": ["hand-over"], "with an argument": []},
}
NOT_LINE = re.compile(r"^\s*(?:[-*]\s+)?\*Not:\*(.*)$", re.M)
STRUCK = re.compile(r"~~([^~]+)~~(?:\s*\((hq|tools)\))?")
IDENT_LINE = re.compile(r"^\s*(?:[-*]\s+)?\*Identifier until renamed:\*(.*)$", re.M)
CODE_SPAN = re.compile(r"`([^`]+)`")
HEAD = re.compile(r"^\s*-\s+\*\*([^*]+)\*\*", re.M)
def rel(path):
return os.path.relpath(path, ROOT)
def read(path):
with open(os.path.join(ROOT, path), encoding="utf-8") as handle:
return handle.read()
def frontmatter_field(text, name):
if not text.startswith("---\n"):
return None
end = text.find("\n---", 4)
m = re.search(r"^%s:\s*(\S+)" % name, text[4:end], re.M)
return m.group(1) if m else None
def glossary():
"""The retired words with their scope, the identifiers, and the head words."""
text = read(GLOSSARY)
retired = []
for line in NOT_LINE.findall(text):
for word, scope in STRUCK.findall(line):
for one in word.split(" / "):
retired.append((one.strip(), scope or "all"))
identifiers = []
for line in IDENT_LINE.findall(text):
identifiers += [i.strip() for i in CODE_SPAN.findall(line)]
heads = []
for head in HEAD.findall(text):
heads += [h.strip() for h in head.split(" / ")]
return retired, identifiers, heads
def pattern(word):
"""A whole-word, case-insensitive match; a space in the word matches a space, a line break or a hyphen.
The plural is the word too: its last part may end in `s` or `es` ("alerts", "control planes"), since
a retired word does not come back by being counted. Anything else joined on is another word.
"""
parts = [re.escape(p) for p in word.split()]
return re.compile(r"(?i)(?<![\w-])" + r"[\s-]+".join(parts) + r"(?:e?s)?(?![\w-])")
def blank(match):
return re.sub(r"[^\n]", " ", match.group(0))
def prose(text, glossary_file=False):
"""The text with everything that is not running prose blanked, keeping offsets and line numbers."""
if text.startswith("---\n"):
end = text.find("\n---", 4)
if end != -1:
text = re.sub(r"[^\n]", " ", text[: end + 4]) + text[end + 4:]
steps = [
r"(?ms)^\s*```.*?^\s*```", # code blocks
r"(?s)<!--.*?-->", # comments
r"(?m)^\s*>.*$", # block quotes
r"`[^`\n]+`", # code spans
r"\]\([^)\s]*\)", # link targets
r"~~[^~\n]+~~", # struck through: a word named as retired
r"\"[^\"\n]*\"", # "quoted"
r"\u201c[^\u201d]*\u201d", # curly double quotes
r"\u2018[^\u2019\n]*\u2019", # curly single quotes
]
if glossary_file:
steps[:0] = [NOT_LINE.pattern, IDENT_LINE.pattern]
for step in steps:
text = re.sub(step, blank, text)
return text
def issues(since=FROM):
"""The issue reports opened on or after `since`, file by file."""
out = []
base = os.path.join(ROOT, "04-ISSUES")
if not os.path.isdir(base):
return out
for issue in sorted(os.listdir(base)):
head = os.path.join(base, issue, "00-report.md")
if not os.path.isfile(head):
continue
date = frontmatter_field(read(rel(head)), "opened")
if not date or date < since:
continue
for name in sorted(os.listdir(os.path.join(base, issue))):
if name.endswith(".md"):
out.append("04-ISSUES/%s/%s" % (issue, name))
return out
def checked_documents(since=FROM):
docs = []
for entry in ALWAYS:
path = os.path.join(ROOT, entry)
if os.path.isfile(path):
docs.append(entry)
continue
for base, dirs, files in os.walk(path):
dirs.sort()
for name in sorted(files):
if name.endswith(".md"):
docs.append(rel(os.path.join(base, name)))
return docs + issues(since)
def allowed():
"""words-allowed.md: one row per document — | `document` | until | why |.
`until` is a date, after which the allowance fails like the words themselves.
"""
out, problems = {}, []
if not os.path.isfile(os.path.join(ROOT, ALLOWED)):
return out, problems
today = datetime.date.today().isoformat()
for line in read(ALLOWED).splitlines():
cells = [c.strip() for c in line.strip().strip("|").split("|")]
if len(cells) != 3 or not cells[0].startswith("`"):
continue
doc, until, why = cells[0].strip("`"), cells[1], cells[2]
if not why:
problems.append("%s: the allowance for %s gives no reason" % (ALLOWED, doc))
if not re.match(r"^\d{4}-\d{2}-\d{2}$", until):
problems.append("%s: the allowance for %s has no date" % (ALLOWED, doc))
continue
elif until < today:
problems.append("%s: the allowance for %s ran out on %s — reword it" % (ALLOWED, doc, until))
continue
out[doc] = until
return out, problems
def check_retired(retired, identifiers):
problems = []
allowance, problems_allowed = allowed()
problems += problems_allowed
words = [(w, pattern(w), "retired") for w, scope in retired if scope in ("all", "hq")]
words += [(i, pattern(i), "an identifier, allowed only in a code span") for i in identifiers]
used = set()
for doc in checked_documents():
text = prose(read(doc), glossary_file=(doc == GLOSSARY))
for word, rx, why in words:
for m in rx.finditer(text):
if doc in allowance:
used.add(doc)
continue
line = text.count("\n", 0, m.start()) + 1
problems.append("%s:%d: %r is %s (glossary)" % (doc, line, m.group(0).replace("\n", " "), why))
for doc in allowance:
if doc not in used:
problems.append("%s: %s no longer uses a retired word — remove its allowance" % (ALLOWED, doc))
return problems
def lines_list():
"""The daemons' lines as the catalogue's copy lists them: `<daemon> <command>`, ` …` when an argument must follow."""
out = []
for daemon, kinds in DAEMON_LINES.items():
out += ["%s %s" % (daemon, c) for c in kinds["alone"]]
out += ["%s %s …" % (daemon, c) for c in kinds["with an argument"]]
return sorted(out)
def line_pattern(daemon, command, with_argument):
"""The line whole: the daemon, white space, the command. One that is a line only with an argument is matched
as a line is written — at the start of a line or a code span, a prompt allowed, an argument after it."""
d, c = re.escape(daemon), re.escape(command)
if with_argument:
return re.compile(r"(?m)(?:^|`)[ \t]*(?:\$[ \t]*)?(" + d + r"\s+" + c + r"\s+[^\s`])")
return re.compile(r"(?<![\w-])" + d + r"\s+" + c + r"(?![\w-])")
def own_words(text):
"""The text with what is not the document's own words blanked: as prose(), but a code block and a code span
are kept, since a line to type is written in one."""
if text.startswith("---\n"):
end = text.find("\n---", 4)
if end != -1:
text = re.sub(r"[^\n]", " ", text[: end + 4]) + text[end + 4:]
for step in (
r"(?s)<!--.*?-->", # comments
r"(?m)^\s*>.*$", # block quotes
r"\]\([^)\s]*\)", # link targets
r"~~[^~\n]+~~", # struck through
r"\"[^\"\n]*\"", # "quoted"
r"\u201c[^\u201d]*\u201d", # curly double quotes
r"\u2018[^\u2019\n]*\u2019", # curly single quotes
):
text = re.sub(step, blank, text)
return text
def check_lines():
problems = []
patterns = []
for daemon, kinds in DAEMON_LINES.items():
patterns += [(daemon, c, line_pattern(daemon, c, False)) for c in kinds["alone"]]
patterns += [(daemon, c, line_pattern(daemon, c, True)) for c in kinds["with an argument"]]
for doc in checked_documents(LINES_FROM):
text = own_words(read(doc))
for daemon, command, rx in patterns:
for m in rx.finditer(text):
line = text.count("\n", 0, m.start()) + 1
problems.append("%s:%d: `%s %s` is a daemon's line — the operator types `nox …` (ADR 0272)"
% (doc, line, daemon, command))
return problems
def check_unique(retired, heads):
problems = []
seen = {}
for head in heads:
key = head.lower()
if key in seen:
problems.append("%s: %r heads two entries" % (GLOSSARY, head))
seen[key] = True
for word, _ in retired:
if word.lower() in seen:
problems.append("%s: %r is a head word and also retired" % (GLOSSARY, word))
return problems
def tools_list(retired):
return sorted({w for w, scope in retired if scope in ("all", "tools")}, key=str.lower)
def check_copy(retired):
catalogue = os.environ.get("MESH_CATALOG_DIR")
if not catalogue:
print("NOT COMPARED: MESH_CATALOG_DIR is not set, so the catalogue's copy of the list was not read")
return []
path = os.path.join(catalogue, "retired-words")
try:
with open(path, encoding="utf-8") as handle:
copy = [l.strip() for l in handle if l.strip() and not l.startswith("#")]
except OSError as err:
return ["the catalogue's copy of the retired words cannot be read: %s" % err]
want = tools_list(retired)
problems = []
if sorted(copy, key=str.lower) != want:
problems.append("the catalogue's retired-words differs from the glossary: it should list %s" % ", ".join(want))
path = os.path.join(catalogue, "retired-lines")
try:
with open(path, encoding="utf-8") as handle:
copy = [l.strip() for l in handle if l.strip() and not l.startswith("#")]
except OSError as err:
return problems + ["the catalogue's copy of the daemons' lines cannot be read: %s" % err]
if sorted(copy) != lines_list():
problems.append("the catalogue's retired-lines differs from this check's list: regenerate it with "
"`python3 00-META/checks/words.py --list lines`")
return problems
def main(argv):
retired, identifiers, heads = glossary()
if argv[1:2] == ["--list"]:
scope = argv[2] if len(argv) > 2 else "tools"
if scope == "lines":
words = lines_list()
else:
words = tools_list(retired) if scope == "tools" else sorted({w for w, s in retired if s in ("all", scope)})
print("\n".join(words))
return 0
if not retired:
print("words: the glossary names no retired word on a *Not:* line — the check would pass on anything")
return 1
problems = check_unique(retired, heads) + check_retired(retired, identifiers) + check_lines() + check_copy(retired)
for p in problems:
print(p)
if problems:
print("words: %d problem(s)" % len(problems))
return 1
print("words: %d retired words and %d identifiers, none in running prose; %d daemons' lines, none written; "
"%d head words, each once" % (len(retired), len(identifiers), len(lines_list()), len(heads)))
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv))
+756
View File
@@ -0,0 +1,756 @@
package checks
import (
"fmt"
"io/fs"
"os"
"path"
"regexp"
"runtime"
"sort"
"strings"
"sync"
"testing"
"testing/fstest"
"time"
"unicode"
)
// One word per thing, checked (words.py until ADR 0527; ADR 0244). The glossary (00-META/glossary.md) is the
// authority on the mesh's words. It names the words it retired on one fixed kind of line, so a program can read them:
//
// *Not:* ~~control plane~~, ~~overlay~~ (hq)
//
// A struck word with no scope is retired everywhere; `(hq)` retires it in this repository's prose only; `(tools)`
// in the descriptions of the mesh's tools only. And it names code that still carries an old name until it is renamed:
//
// *Identifier until renamed:* `mesh-host` - the repository, binary and service unit
//
// An identifier is allowed inside a code span and nowhere else.
//
// What is enforced:
//
// retired no retired word of scope hq (or none), and no identifier, in running prose of a checked document.
// Running prose is what is left once code blocks, code spans, block quotes, text in quotation marks,
// struck-through text, link targets, HTML comments and frontmatter are taken out. A word's plural is the word.
// unique no head word heads two entries, and none is also struck through on a *Not:* line.
// lines no daemon's line in a checked document (ADR 0272, its insight of 2026-10-09): the operator types `nox`.
// copy with MESH_CATALOG_DIR set to a checkout of the catalog, its copies of the retired words and the
// daemons' lines equal this glossary's and this list (skipped, with its reason, when unset).
//
// Checked documents: 00-META/, 03-DESIGN/ (both layers), AGENTS.md and README.md, always; an issue opened on or after
// wordsFrom (on or after linesFrom for the lines). A document that cannot be reworded in the change that found it is
// named, with a date and a reason, in words-allowed.md beside this file; an entry past its date fails like the word
// itself, and so does an entry for a document that no longer needs it.
const (
glossaryFile = "00-META/glossary.md"
allowedFile = "00-META/checks/words-allowed.md"
// The day the rule began (ADR 0244): issues opened from it on are held to it.
wordsFrom = "2026-10-07"
// The day the daemons' lines rule began.
linesFrom = "2026-10-09"
)
var wordsAlways = []string{"00-META/", "03-DESIGN/", "AGENTS.md", "README.md"}
// daemonLines are the daemons' lines (ADR 0272): each daemon's commands. "alone" is a command that is a line by
// itself; "with an argument" one that is a line only when written as one ("the mesh-controller module" is prose;
// `mesh-controller module check ...` is a line). The node-engine's install-time subcommands are not lines here.
var daemonLines = []struct {
daemon, alone, withArgument string
}{
{"mesh-controller",
"adopt assign broker cancel cleanup collect conditions converge doctor durations hand-act hand-acts healers identity kill licence " +
"migrate operator pause pin plan prepare push rebuild rehearse replay resume retire rollout rotate secret settings status token " +
"unassign unpin upgrade",
"artifacts ask builds bus collection data delivery images mirrors module node overlay plans queue seat seats take version"},
{"mesh-host", "apply bundle enrol enroll hand-over help inventory key overlay owned profile reconcile run version", ""},
{"nox-mesh-host", "hand-over", ""},
}
var (
notLineRx = regexp.MustCompile(`(?m)^\s*(?:[-*]\s+)?\*Not:\*(.*)$`)
struckRx = regexp.MustCompile(`~~([^~]+)~~(?:\s*\((hq|tools)\))?`)
identLineRx = regexp.MustCompile(`(?m)^\s*(?:[-*]\s+)?\*Identifier until renamed:\*(.*)$`)
spanRx = regexp.MustCompile("`([^`]+)`")
headRx = regexp.MustCompile(`(?m)^\s*-\s+\*\*([^*]+)\*\*`)
openedRx = regexp.MustCompile(`(?m)^opened:\s*(\S+)`)
)
type retiredWord struct{ word, scope string }
// parseGlossary reads the retired words with their scope, the identifiers and the head words from the glossary.
func parseGlossary(text string) (retired []retiredWord, identifiers, heads []string) {
for _, m := range notLineRx.FindAllStringSubmatch(text, -1) {
for _, s := range struckRx.FindAllStringSubmatch(m[1], -1) {
scope := s[2]
if scope == "" {
scope = "all"
}
for _, one := range strings.Split(s[1], " / ") {
retired = append(retired, retiredWord{strings.TrimSpace(one), scope})
}
}
}
for _, m := range identLineRx.FindAllStringSubmatch(text, -1) {
for _, s := range spanRx.FindAllStringSubmatch(m[1], -1) {
identifiers = append(identifiers, strings.TrimSpace(s[1]))
}
}
for _, m := range headRx.FindAllStringSubmatch(text, -1) {
for _, h := range strings.Split(m[1], " / ") {
heads = append(heads, strings.TrimSpace(h))
}
}
return
}
func isWordRune(r rune) bool {
return unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' || r == '-'
}
// findBounded returns the start offsets of the matches of rx that are whole words: no word character or hyphen
// before or after (the lookarounds of the Python check, which RE2 has not).
func findBounded(rx *regexp.Regexp, text string) []int {
var out []int
pos := 0
for pos <= len(text) {
loc := rx.FindStringIndex(text[pos:])
if loc == nil {
break
}
start, end := pos+loc[0], pos+loc[1]
before, after := true, true
if start > 0 {
r := []rune(text[max(0, start-4):start])
before = !isWordRune(r[len(r)-1])
}
if end < len(text) {
r := []rune(text[end:min(len(text), end+4)])
after = !isWordRune(r[0])
}
if before && after {
out = append(out, start)
pos = end
if end == start {
pos++
}
} else {
pos = start + 1
}
}
return out
}
// wordRegexp matches a word whole, case-insensitively; a space in the word matches a space, a line break or a hyphen.
// The plural is the word too: its last part may end in s or es ("alerts", "control planes").
func wordRegexp(word string) *regexp.Regexp {
var parts []string
for _, p := range strings.Fields(word) {
parts = append(parts, regexp.QuoteMeta(p))
}
return regexp.MustCompile(`(?i)` + strings.Join(parts, `[\s-]+`) + `(?:e?s)?`)
}
// blank replaces every byte of s but the line breaks, so offsets and line numbers hold.
func blank(s string) string {
b := []byte(s)
for i, c := range b {
if c != '\n' {
b[i] = ' '
}
}
return string(b)
}
func blankFrontmatter(text string) string {
if strings.HasPrefix(text, "---\n") {
if end := strings.Index(text[4:], "\n---"); end != -1 {
end += 4
return blank(text[:end+4]) + text[end+4:]
}
}
return text
}
func blankAll(text string, steps []*regexp.Regexp) string {
for _, rx := range steps {
text = rx.ReplaceAllStringFunc(text, blank)
}
return text
}
func compileAll(patterns ...string) []*regexp.Regexp {
var out []*regexp.Regexp
for _, p := range patterns {
out = append(out, regexp.MustCompile(p))
}
return out
}
// What is not the document's own words, in the order it is taken out.
const (
stepComment = `(?s)<!--.*?-->`
stepBlockQuote = `(?m)^\s*>.*$`
stepLinkTarget = `\]\([^)\s]*\)`
stepStruck = `~~[^~\n]+~~`
stepQuoted = `"[^"\n]*"`
stepCurlyDouble = `\x{201c}[^\x{201d}]*\x{201d}`
stepCurlySingle = `\x{2018}[^\x{2019}\n]*\x{2019}`
)
var (
proseSteps = compileAll("(?ms)^\\s*```.*?^\\s*```", stepComment, stepBlockQuote, "`[^`\\n]+`", stepLinkTarget, stepStruck,
stepQuoted, stepCurlyDouble, stepCurlySingle)
// In the glossary the *Not:* and *Identifier until renamed:* lines name the retired words, so they are taken out first.
glossaryProseSteps = append(compileAll(notLineRx.String(), identLineRx.String()), proseSteps...)
// The daemons' lines are looked for in the document's own words: a code block and a code span are kept, since a
// line to type is written in one.
ownWordsSteps = compileAll(stepComment, stepBlockQuote, stepLinkTarget, stepStruck, stepQuoted, stepCurlyDouble, stepCurlySingle)
)
// prose is the text with everything that is not running prose blanked, keeping offsets and line numbers.
func prose(text string, glossaryDoc bool) string {
text = blankFrontmatter(text)
if glossaryDoc {
return blankAll(text, glossaryProseSteps)
}
return blankAll(text, proseSteps)
}
func ownWords(text string) string { return blankAll(blankFrontmatter(text), ownWordsSteps) }
// issueDocuments lists the issue reports' files opened on or after since.
func issueDocuments(fsys fs.FS, since string) []string {
var out []string
entries, _ := fs.ReadDir(fsys, "04-ISSUES")
for _, e := range entries {
if !e.IsDir() {
continue
}
head := readFile(fsys, path.Join("04-ISSUES", e.Name(), "00-report.md"))
front := ""
if strings.HasPrefix(head, "---\n") {
if end := strings.Index(head[4:], "\n---"); end != -1 {
if m := openedRx.FindStringSubmatch(head[4 : 4+end]); m != nil {
front = m[1]
}
}
}
if front == "" || front < since {
continue
}
files, _ := fs.ReadDir(fsys, path.Join("04-ISSUES", e.Name()))
for _, f := range files {
if strings.HasSuffix(f.Name(), ".md") {
out = append(out, path.Join("04-ISSUES", e.Name(), f.Name()))
}
}
}
return out
}
// perDocument runs fn over the documents on all processors (the unit level is short) and joins the problems in
// the documents' order.
func perDocument(docs []string, fn func(doc string) []string) []string {
results := make([][]string, len(docs))
var wg sync.WaitGroup
sem := make(chan struct{}, runtime.NumCPU())
for i, d := range docs {
wg.Add(1)
sem <- struct{}{}
go func() {
defer wg.Done()
results[i] = fn(d)
<-sem
}()
}
wg.Wait()
var out []string
for _, r := range results {
out = append(out, r...)
}
return out
}
func checkedDocuments(fsys fs.FS, since string) []string {
var docs []string
for _, entry := range wordsAlways {
if strings.HasSuffix(entry, "/") {
_ = fs.WalkDir(fsys, strings.TrimSuffix(entry, "/"), func(p string, d fs.DirEntry, err error) error {
if err == nil && !d.IsDir() && strings.HasSuffix(p, ".md") {
docs = append(docs, p)
}
return nil
})
} else if _, err := fs.Stat(fsys, entry); err == nil {
docs = append(docs, entry)
}
}
return append(docs, issueDocuments(fsys, since)...)
}
var dateRx = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}$`)
// parseAllowed reads words-allowed.md: one row per document, | `document` | until | why |. Until is a date, after
// which the allowance fails like the words themselves. The clock is the caller's: today is YYYY-MM-DD.
func parseAllowed(text, today string) (allowed map[string]string, problems []string) {
allowed = map[string]string{}
for _, line := range strings.Split(text, "\n") {
cells := strings.Split(strings.Trim(strings.TrimSpace(line), "|"), "|")
if len(cells) != 3 {
continue
}
for i := range cells {
cells[i] = strings.TrimSpace(cells[i])
}
if !strings.HasPrefix(cells[0], "`") {
continue
}
doc, until, why := strings.Trim(cells[0], "`"), cells[1], cells[2]
if why == "" {
problems = append(problems, fmt.Sprintf("%s: the allowance for %s gives no reason", allowedFile, doc))
}
if !dateRx.MatchString(until) {
problems = append(problems, fmt.Sprintf("%s: the allowance for %s has no date", allowedFile, doc))
continue
}
if until < today {
problems = append(problems, fmt.Sprintf("%s: the allowance for %s ran out on %s — reword it", allowedFile, doc, until))
continue
}
allowed[doc] = until
}
return
}
func checkRetired(fsys fs.FS, retired []retiredWord, identifiers []string, today string) []string {
allowance, out := parseAllowed(readFile(fsys, allowedFile), today)
type matcher struct {
word string
rx *regexp.Regexp
why string
}
var words []matcher
for _, r := range retired {
if r.scope == "all" || r.scope == "hq" {
words = append(words, matcher{r.word, wordRegexp(r.word), "retired"})
}
}
for _, i := range identifiers {
words = append(words, matcher{i, wordRegexp(i), "an identifier, allowed only in a code span"})
}
var mu sync.Mutex
used := map[string]bool{}
out = append(out, perDocument(checkedDocuments(fsys, wordsFrom), func(d string) []string {
var found []string
text := prose(readFile(fsys, d), d == glossaryFile)
lower := strings.ToLower(text)
for _, w := range words {
// Every match holds the word's first part literally, so a text without it is not searched (the unit level is short).
if !strings.Contains(lower, strings.ToLower(strings.Fields(w.word)[0])) {
continue
}
for _, start := range findBounded(w.rx, text) {
if _, ok := allowance[d]; ok {
mu.Lock()
used[d] = true
mu.Unlock()
continue
}
m := w.rx.FindString(text[start:])
found = append(found, fmt.Sprintf("%s:%d: %q is %s (glossary)", d, lineOf(text, start), strings.ReplaceAll(m, "\n", " "), w.why))
}
}
return found
})...)
var unused []string
for d := range allowance {
if !used[d] {
unused = append(unused, d)
}
}
sort.Strings(unused)
for _, d := range unused {
out = append(out, fmt.Sprintf("%s: %s no longer uses a retired word — remove its allowance", allowedFile, d))
}
return out
}
func checkUnique(retired []retiredWord, heads []string) []string {
var out []string
seen := map[string]bool{}
for _, h := range heads {
k := strings.ToLower(h)
if seen[k] {
out = append(out, fmt.Sprintf("%s: %q heads two entries", glossaryFile, h))
}
seen[k] = true
}
for _, r := range retired {
if seen[strings.ToLower(r.word)] {
out = append(out, fmt.Sprintf("%s: %q is a head word and also retired", glossaryFile, r.word))
}
}
return out
}
// linesList is the daemons' lines as the catalog's copy lists them: "<daemon> <command>", with " …" when an
// argument must follow.
func linesList() []string {
var out []string
for _, d := range daemonLines {
for _, c := range strings.Fields(d.alone) {
out = append(out, d.daemon+" "+c)
}
for _, c := range strings.Fields(d.withArgument) {
out = append(out, d.daemon+" "+c+" …")
}
}
sort.Strings(out)
return out
}
func checkLines(fsys fs.FS) []string {
type pat struct {
daemon, command string
rx *regexp.Regexp
bounded bool
}
var pats []pat
for _, d := range daemonLines {
dq := regexp.QuoteMeta(d.daemon)
for _, c := range strings.Fields(d.alone) {
pats = append(pats, pat{d.daemon, c, regexp.MustCompile(dq + `\s+` + regexp.QuoteMeta(c)), true})
}
for _, c := range strings.Fields(d.withArgument) {
// A line only when written as one: at the start of a line or a code span, a prompt allowed, an argument after.
pats = append(pats, pat{d.daemon, c, regexp.MustCompile("(?m)(?:^|`)[ \\t]*(?:\\$[ \\t]*)?(" + dq + `\s+` + regexp.QuoteMeta(c) + "\\s+[^\\s`])"), false})
}
}
return perDocument(checkedDocuments(fsys, linesFrom), func(d string) []string {
var out []string
text := ownWords(readFile(fsys, d))
for _, p := range pats {
if !strings.Contains(text, p.daemon) {
continue
}
var starts []int
if p.bounded {
starts = findBounded(p.rx, text)
} else {
for _, loc := range p.rx.FindAllStringSubmatchIndex(text, -1) {
starts = append(starts, loc[2])
}
}
for _, s := range starts {
out = append(out, fmt.Sprintf("%s:%d: `%s %s` is a daemon's line — the operator types `nox …` (ADR 0272)", d, lineOf(text, s), p.daemon, p.command))
}
}
return out
})
}
func toolsList(retired []retiredWord) []string {
seen := map[string]bool{}
var out []string
for _, r := range retired {
if (r.scope == "all" || r.scope == "tools") && !seen[r.word] {
seen[r.word] = true
out = append(out, r.word)
}
}
sort.Slice(out, func(i, j int) bool { return strings.ToLower(out[i]) < strings.ToLower(out[j]) })
return out
}
func readList(file string) ([]string, error) {
b, err := os.ReadFile(file)
if err != nil {
return nil, err
}
var out []string
for _, l := range strings.Split(string(b), "\n") {
if l = strings.TrimSpace(l); l != "" && !strings.HasPrefix(l, "#") {
out = append(out, l)
}
}
return out, nil
}
// checkCopy compares the catalog's copies of the retired words and the daemons' lines with this glossary's. A
// difference names what the copy should list, so regenerating it is copying that from the failure.
func checkCopy(catalogue string, retired []retiredWord) []string {
var out []string
copyWords, err := readList(path.Join(catalogue, "retired-words"))
if err != nil {
return []string{"the catalog's copy of the retired words cannot be read: " + err.Error()}
}
sort.Slice(copyWords, func(i, j int) bool { return strings.ToLower(copyWords[i]) < strings.ToLower(copyWords[j]) })
want := toolsList(retired)
if strings.Join(copyWords, "\n") != strings.Join(want, "\n") {
out = append(out, "the catalog's retired-words differs from the glossary: it should list "+strings.Join(want, ", "))
}
copyLines, err := readList(path.Join(catalogue, "retired-lines"))
if err != nil {
return append(out, "the catalog's copy of the daemons' lines cannot be read: "+err.Error())
}
sort.Strings(copyLines)
if strings.Join(copyLines, "\n") != strings.Join(linesList(), "\n") {
out = append(out, "the catalog's retired-lines differs from this check's list: it should list, one per line:\n"+strings.Join(linesList(), "\n"))
}
return out
}
// The real tree.
func today() string { return time.Now().Format("2006-01-02") }
func TestWordsGlossaryNamesRetiredWords(t *testing.T) {
retired, identifiers, heads := parseGlossary(readFile(rootFS(), glossaryFile))
if len(retired) == 0 {
t.Fatal("the glossary names no retired word on a *Not:* line — the check would pass on anything")
}
if len(identifiers) == 0 || len(heads) == 0 {
t.Errorf("the glossary names %d identifiers and %d head words; both are expected", len(identifiers), len(heads))
}
}
func TestWordsEachHeadWordHeadsOneEntry(t *testing.T) {
retired, _, heads := parseGlossary(readFile(rootFS(), glossaryFile))
expectNone(t, checkUnique(retired, heads))
}
func TestWordsNoRetiredWordInRunningProse(t *testing.T) {
retired, identifiers, _ := parseGlossary(readFile(rootFS(), glossaryFile))
expectNone(t, checkRetired(rootFS(), retired, identifiers, today()))
}
func TestWordsNoDaemonLineWritten(t *testing.T) { expectNone(t, checkLines(rootFS())) }
func TestWordsCatalogueCopyAgrees(t *testing.T) {
catalogue := os.Getenv("MESH_CATALOG_DIR")
if catalogue == "" {
t.Skip("NOT COMPARED: MESH_CATALOG_DIR is not set, so the catalog's copy of the list was not read")
}
retired, _, _ := parseGlossary(readFile(rootFS(), glossaryFile))
expectNone(t, checkCopy(catalogue, retired))
}
// The checks themselves, against broken trees (H5a: the glossary).
const glossaryFixture = `# Glossary
- **control plane** — the retired one's replacement is below.
*Not:* ~~control plane~~, ~~overlay~~ (hq), ~~alert~~ (tools), ~~a / b~~
- **controller** — decides.
*Not:* ~~control plane~~
- **Controller** — again.
- *Identifier until renamed:* ` + "`mesh-host`, `nox-old`" + ` — the repository
`
func TestGlossaryReadsNotLinesScopesIdentifiersAndHeads(t *testing.T) {
retired, identifiers, heads := parseGlossary(glossaryFixture)
want := []retiredWord{{"control plane", "all"}, {"overlay", "hq"}, {"alert", "tools"}, {"a", "all"}, {"b", "all"}, {"control plane", "all"}}
if fmt.Sprint(retired) != fmt.Sprint(want) {
t.Errorf("retired = %v, want %v", retired, want)
}
if fmt.Sprint(identifiers) != "[mesh-host nox-old]" {
t.Errorf("identifiers = %v", identifiers)
}
if fmt.Sprint(heads) != "[control plane controller Controller]" {
t.Errorf("heads = %v", heads)
}
got := checkUnique(retired, heads)
expectProblem(t, got, `"Controller" heads two entries`)
expectProblem(t, got, `"control plane" is a head word and also retired`)
}
func TestToolsListHoldsTheWordsOfScopeAllAndTools(t *testing.T) {
retired, _, _ := parseGlossary(glossaryFixture)
if got := fmt.Sprint(toolsList(retired)); got != "[a alert b control plane]" {
t.Errorf("toolsList = %s", got)
}
}
// H5b: prose blanking and matching.
func TestProseBlanksWhatIsNotRunningProse(t *testing.T) {
text := "---\nfm: overlay\n---\n" +
"bare overlay one\n" +
"```\noverlay in a block\n```\n" +
"span `overlay` x\n" +
"> quoted overlay\n" +
"say \"overlay\" and “overlay” and ‘overlay’\n" +
"~~overlay~~ struck\n" +
"[link](overlay.md)\n" +
"<!-- overlay\nover lines -->\n" +
"bare overlay two\n"
got := prose(text, false)
if strings.Count(got, "overlay") != 2 {
t.Errorf("only the two bare uses stay: %q", got)
}
if strings.Count(got, "\n") != strings.Count(text, "\n") || len(got) != len(text) {
t.Error("blanking keeps offsets and line numbers")
}
}
func TestProseOfTheGlossaryBlanksNotAndIdentifierLines(t *testing.T) {
got := prose("- **x** — y\n *Not:* control plane, plain\n- *Identifier until renamed:* mesh-host\nthe control plane\n", true)
if strings.Count(got, "control plane") != 1 || strings.Contains(got, "mesh-host") {
t.Errorf("only the running use stays: %q", got)
}
}
func TestWordMatchesWholeWordsPluralsAndLineBreaks(t *testing.T) {
rx := wordRegexp("control plane")
count := func(s string) int { return len(findBounded(rx, s)) }
for s, want := range map[string]int{
"the control plane runs": 1,
"Control Planes": 1,
"control-plane and control\nplane": 2,
"uncontrol plane": 0,
"control planet": 0,
"control plane-x": 0,
"x-control plane": 0,
"control planexs": 0,
} {
if got := count(s); got != want {
t.Errorf("%q: %d matches, want %d", s, got, want)
}
}
}
func TestRetiredCatchesAWordInProseAndNamesDocumentAndLine(t *testing.T) {
retired := []retiredWord{{"control plane", "all"}, {"overlay", "hq"}, {"alert", "tools"}}
broken := mapOf(map[string]string{
"AGENTS.md": "fine\nthe control plane here\n",
"03-DESIGN/01-to-be/a.md": "an overlay and \"a control plane quoted\" and `overlay` alert\n",
"README.md": "mesh-host runs; `mesh-host` too\n",
})
got := checkRetired(broken, retired, []string{"mesh-host"}, "2026-10-23")
expectProblem(t, got, `AGENTS.md:2: "control plane" is retired`)
expectProblem(t, got, `03-DESIGN/01-to-be/a.md:1: "overlay" is retired`)
expectProblem(t, got, `README.md:1: "mesh-host" is an identifier, allowed only in a code span`)
if len(got) != 3 {
t.Errorf("a quotation, a code span and a word of scope tools are not problems: %q", got)
}
}
func TestIssuesAreHeldToTheRuleFromItsDay(t *testing.T) {
retired := []retiredWord{{"overlay", "all"}}
fsys := mapOf(map[string]string{
"04-ISSUES/1-old/00-report.md": "---\nopened: 2026-10-06\n---\noverlay\n",
"04-ISSUES/2-new/00-report.md": "---\nopened: 2026-10-07\n---\nfine\n",
"04-ISSUES/2-new/01-notes.md": "an overlay\n",
"04-ISSUES/3-nodate/00-report.md": "overlay\n",
})
got := checkRetired(fsys, retired, nil, "2026-10-23")
expectProblem(t, got, "04-ISSUES/2-new/01-notes.md:1")
if len(got) != 1 {
t.Errorf("only the issue opened from 2026-10-07 on is read: %q", got)
}
}
// H5c: allowances, with an injected clock.
const allowedFixture = `# Words allowed
| Document | Until | Why |
|---|---|---|
| ` + "`a.md`" + ` | 2026-12-01 | quotes what was |
| ` + "`b.md`" + ` | 2026-10-01 | ran out |
| ` + "`c.md`" + ` | soon | no date |
| ` + "`d.md`" + ` | 2026-12-01 | |
`
func TestAllowedReadsRowsAndTheInjectedClock(t *testing.T) {
allowed, problems := parseAllowed(allowedFixture, "2026-10-23")
if _, ok := allowed["a.md"]; !ok || len(allowed) != 2 {
t.Errorf("a.md and d.md are in date: %v", allowed)
}
expectProblem(t, problems, "the allowance for b.md ran out on 2026-10-01")
expectProblem(t, problems, "the allowance for c.md has no date")
expectProblem(t, problems, "the allowance for d.md gives no reason")
// The same text on an earlier day: b.md is still in date.
if early, _ := parseAllowed(allowedFixture, "2026-09-30"); len(early) != 3 {
t.Errorf("on 2026-09-30 a, b and d are in date: %v", early)
}
}
func TestAllowanceHoldsAWordBackUntilItsDateAndShrinks(t *testing.T) {
retired := []retiredWord{{"overlay", "all"}}
allowedRow := "| `AGENTS.md` | 2026-12-01 | quotes what was |\n"
tree := func(agents, allowed string) map[string]string {
return map[string]string{"AGENTS.md": agents, allowedFile: allowed}
}
expectNone(t, checkRetired(mapOf(tree("an overlay\n", allowedRow)), retired, nil, "2026-10-23"))
expectProblem(t, checkRetired(mapOf(tree("an overlay\n", allowedRow)), retired, nil, "2026-12-02"), "ran out on 2026-12-01")
expectProblem(t, checkRetired(mapOf(tree("clean\n", allowedRow)), retired, nil, "2026-10-23"), "AGENTS.md no longer uses a retired word")
expectProblem(t, checkRetired(mapOf(tree("an overlay\n", "")), retired, nil, "2026-10-23"), `AGENTS.md:1: "overlay" is retired`)
}
// H5d: the daemons' lines and the catalog comparison.
func TestLinesCatchesADaemonsLineButNotProse(t *testing.T) {
fsys := mapOf(map[string]string{
"AGENTS.md": "the mesh-controller module is prose\n" +
"run `mesh-controller module check x` now\n" +
"or:\n```\n$ mesh-controller push\n```\n" +
"a quotation \"mesh-controller status\" stays\n" +
"> mesh-controller doctor in a block quote\n" +
"a mesh-controller pushed is another word\n",
"04-ISSUES/1/00-report.md": "---\nopened: 2026-10-08\n---\nmesh-host apply\n",
"04-ISSUES/2/00-report.md": "---\nopened: 2026-10-09\n---\nmesh-host apply\n",
})
got := checkLines(fsys)
expectProblem(t, got, "AGENTS.md:2: `mesh-controller module` is a daemon's line")
expectProblem(t, got, "AGENTS.md:5: `mesh-controller push` is a daemon's line")
expectProblem(t, got, "04-ISSUES/2/00-report.md:4: `mesh-host apply`")
if len(got) != 3 {
t.Errorf("prose, quotations, block quotes and issues before 2026-10-09 are not problems: %q", got)
}
}
func TestLinesListNamesEachLineOnce(t *testing.T) {
list := linesList()
seen := map[string]bool{}
for _, l := range list {
if seen[l] {
t.Errorf("%q listed twice", l)
}
seen[l] = true
}
if !seen["mesh-controller push"] || !seen["mesh-controller module …"] || !seen["nox-mesh-host hand-over"] {
t.Errorf("the list lacks a known line: %v", list[:5])
}
}
func TestCatalogueCopyDifferenceIsCaughtAndNamesTheList(t *testing.T) {
retired := []retiredWord{{"overlay", "all"}, {"alert", "tools"}, {"hq only", "hq"}}
dir := t.TempDir()
write := func(name, content string) {
if err := os.WriteFile(path.Join(dir, name), []byte(content), 0o644); err != nil {
t.Fatal(err)
}
}
write("retired-words", "# copy\nalert\noverlay\n")
write("retired-lines", strings.Join(linesList(), "\n")+"\n")
expectNone(t, checkCopy(dir, retired))
write("retired-words", "alert\n")
write("retired-lines", "mesh-controller push\n")
got := checkCopy(dir, retired)
expectProblem(t, got, "retired-words differs from the glossary: it should list alert, overlay")
expectProblem(t, got, "retired-lines differs from this check's list")
expectProblem(t, checkCopy(t.TempDir(), retired), "cannot be read")
}
func mapOf(files map[string]string) fs.FS {
m := fstest.MapFS{}
for k, v := range files {
m[k] = doc(v)
}
return m
}
+275 -86
View File
@@ -14,7 +14,7 @@ and the domain decides what that word means. Other domains use the word as it is
change its meaning. Every word is defined once, in the domain that owns it; a section lists the words change its meaning. Every word is defined once, in the domain that owns it; a section lists the words
it uses from other domains under *Uses*, with no second definition. it uses from other domains under *Uses*, with no second definition.
Two fixed lines follow an entry where they apply, and `00-META/checks/words.py` reads them: Two fixed lines follow an entry where they apply, and the words test in `00-META/checks/` reads them:
- *Not:* followed by struck-through words — the words this entry replaced. A struck word with no scope - *Not:* followed by struck-through words — the words this entry replaced. A struck word with no scope
is retired everywhere; *(hq)* retires it in this repository's prose only; *(tools)* in the is retired everywhere; *(hq)* retires it in this repository's prose only; *(tools)* in the
@@ -35,7 +35,7 @@ is listed under [Homonyms](#homonyms), with the qualified form each domain uses.
names, except *observability*, which is now **Health and repair**. Where a domain's records live in names, except *observability*, which is now **Health and repair**. Where a domain's records live in
the controller, the domain owns that store alone (ADR 0008). the controller, the domain owns that store alone (ADR 0008).
*Context* in that sense is not used in new writing; the word stays ordinary English, so this is *Context* in that sense is not used in new writing; the word stays ordinary English, so this is
checked by review, not by `words.py`. checked by review, not by the words test.
- **machine** — any computer: hardware, a virtual machine, before, during or outside its membership of - **machine** — any computer: hardware, a virtual machine, before, during or outside its membership of
the mesh. Prose about the computer itself — its disks, its kernel, the network it sits on, what was on the mesh. Prose about the computer itself — its disks, its kernel, the network it sits on, what was on
it before the mesh — says machine. it before the mesh — says machine.
@@ -66,7 +66,7 @@ is listed under [Homonyms](#homonyms), with the qualified form each domain uses.
## Module — what a module is and declares ## Module — what a module is and declares
**Purpose.** To say what one module is, in the one document every other domain reads. **Purpose.** To say what one module is, in the one document every other domain reads.
**Recorded in** the catalogue, one manifest per module. **Upstream of** every other domain; its words **Recorded in** the catalog, one manifest per module. **Upstream of** every other domain; its words
are a published vocabulary, fixed by the manifest's schema. **Decided by** ADR 0040, 0126, 0174, 0188, are a published vocabulary, fixed by the manifest's schema. **Decided by** ADR 0040, 0126, 0174, 0188,
0207, 0236, 0262. 0207, 0236, 0262.
**Uses:** module, seat, config value (Config), provision (Provisioning), data class (Data), health (Health **Uses:** module, seat, config value (Config), provision (Provisioning), data class (Data), health (Health
@@ -104,7 +104,8 @@ and repair).
- **upgrade policy** — how a module's new builds reach its nodes: `roll` (one node first, judged, then - **upgrade policy** — how a module's new builds reach its nodes: `roll` (one node first, judged, then
the rest), `together`, or `record` (register the build, send it nowhere) the rest), `together`, or `record` (register the build, send it nowhere)
(ADR 0236). (ADR 0236).
The controller's verb `upgrade` shows it. The controller's verb `upgrade` shows it until the lease write; from L5's delivery the seat `mesh-wants`,
held once by the assigner, answers it too, as `mesh-wants.upgrade`; a person's choice is kept by the assigner, and the bus is never rolled (ADR 0565 §7).
## Config — the values someone may change ## Config — the values someone may change
@@ -134,11 +135,15 @@ downstream of Module and Placement. **Decided by** ADR 0262, 0277, 0348.
module has moved to their config names (ADR 0348 §1, issue 630). module has moved to their config names (ADR 0348 §1, issue 630).
What a "flavor" once varied is a config value or a separate module. What a "flavor" once varied is a config value or a separate module.
*Not:* ~~flavor~~ *Not:* ~~flavor~~
- **predefined key** — a key of the catalogue's `predefined.json`, the fixed list of keys for which mesh - **config candidate** — the config declarations a pull request's head adds or changes against the trunk's
manifests, kept by the indexer per open pull request in its state `config-candidates`; mesh-config judges
it again for the values proposed for that pull request (ADR 0565 §2, §3).
- **predefined key** — a key of the catalog's `predefined.json`, the fixed list of keys for which mesh
config and node config hold values, each with its type, description, validation and whether it is config and node config hold values, each with its type, description, validation and whether it is
trusted, such as `font-size`. A config value may name the predefined key it falls back to, of its own trusted, such as `font-size`. A config value may name the predefined key it falls back to, of its own
type; one that names none resolves from its default and the module's own value only. The list is the type; one that names none resolves from its default and the module's own value only. The list is the
catalogue build's: a change to it is validated with every manifest naming a key, and removing a key still catalog build's: a change to it is validated with every manifest naming a key, and removing a key still
named or still holding a value is refused (ADR 0348). named or still holding a value is refused (ADR 0348).
- **trusted** — said of a config value that decides what runs or who can reach what. At every level it is - **trusted** — said of a config value that decides what runs or who can reach what. At every level it is
set only at the controller's terminal or through `config` with propose, on the operator's Approve; a set only at the controller's terminal or through `config` with propose, on the operator's Approve; a
@@ -173,8 +178,10 @@ every other domain has something to run on. **Recorded by** the controller and t
mesh. It is not a separate kind of node — it is a node that additionally runs the controller (and, mesh. It is not a separate kind of node — it is a node that additionally runs the controller (and,
today, the foundation). Lose it and the other nodes keep running what they were last told; they simply today, the foundation). Lose it and the other nodes keep running what they were last told; they simply
cannot be told anything new. cannot be told anything new.
- **node-engine** — the program on every node that applies what the controller declares: it receives - **node-engine** — the program on every node that applies what its node should run: from release 6 of
the node's declaration, writes the files, runs the services and containers, and reports what it did. ADR 0429 its node agent follows the node's wants and the compositions the controller signs for it, one module
at a time (ADR 0539); until then it received the node's whole declaration. It writes the files, runs the
services and containers, and says what it runs in its haves.
It is the engine, not a module: it owns no file's content, and every file it writes belongs to the It is the engine, not a module: it owns no file's content, and every file it writes belongs to the
module that declared it. *Agent* is avoided because that word means a coding agent here. A record module that declared it. *Agent* is avoided because that word means a coding agent here. A record
written before the rename keeps the old name. "The host" is retired in this repository's prose only: written before the rename keeps the old name. "The host" is retired in this repository's prose only:
@@ -233,12 +240,31 @@ every other domain has something to run on. **Recorded by** the controller and t
## Placement — what runs on which node ## Placement — what runs on which node
**Purpose.** To decide, send and apply what each node runs, and to say why. **Purpose.** To decide, send and apply what each node runs, and to say why.
**Recorded by** the controller's `inventory` database. **Upstream of** Provisioning, Connectivity, Data **Recorded by** the controller's `inventory` database; from ADR 0567, by the seat `mesh-placement`, in its states on
and Health and repair. **Decided by** ADR 0100, 0126, 0176, 0181, 0207, 0221, 0252, 0261, 0411. the bus. **Upstream of** Provisioning, Connectivity, Data
**Uses:** node, machine, module, seat, manifest (Module), config value (Config). and Health and repair. **Decided by** ADR 0100, 0126, 0176, 0181, 0207, 0221, 0252, 0261, 0411, 0539, 0549, 0559, 0567.
**Uses:** node, machine, module, seat, manifest (Module), config value (Config), proposal (Operator and conversation).
- **assignment** — a module put on a node. Its config values on that node (module-assignment config, - **assignment** — a module put on a node. Its config values on that node (module-assignment config,
Config) are `mesh-config`'s, never part of what the controller records (ADR 0348 §9). Config) are `mesh-config`'s, never part of what the controller records (ADR 0348 §9).
- **placement** — which module runs on which node, with its pins, host ports and bindings, and what a person
decided of the node, as one key per node in the state `placement` of the seat `mesh-placement`. It changes only by
a placement proposal the operator approves, and no repository holds it or names a node (ADR 0567). Until ADR
0547's third milestone the controller's assignments are kept equal to it by the holder.
*Not:* ~~placement file~~ (hq)
- **mesh-placement** — the seat, held once for the mesh by the assigner, that keeps placement: its states
`placement`, `proposals` and `candidates`, and its seven verbs `placement`, `proposals`, `propose`, `withdraw`,
`again`, `adopt` and `hand-back`. Only the holder writes its states (ADR 0567).
- **placement proposal** — a change to placement put forward through the verb `propose`: the changes, the revision
of each key they were laid over, the nodes' answers, the verdict and the ask. It takes effect only on the operator's
Approve, and is discarded if declined, expired or withdrawn. A kind of *proposal* (Operator and conversation).
- **placement candidate** — one node's placement as a placement proposal lays it, kept in the state `candidates`
of the seat `mesh-placement` under `<proposal>.<node>` while the proposal is open. The node composes it in
**shadow** (composed and compared, never applied) and answers in its own have whether what it would run is the
same; the verdict fails only on a refusal or a node that has not answered (ADR 0567). Not the indexer's candidates of
a group, which are fingerprints (Delivery).
- **hand-back** — the operator's verb `hand-back` of the seat `mesh-placement`, answered until the lease write: it
deletes every placement key, and every node returns to the compositions the controller sends. A hand act (ADR 0567).
- **pending assignment** — an assignment of a module whose build is still running, kept, and made by the - **pending assignment** — an assignment of a module whose build is still running, kept, and made by the
controller when that build registers the module, or ended with why when it does not controller when that build registers the module, or ended with why when it does not
(ADR 0261). (ADR 0261).
@@ -259,14 +285,33 @@ and Health and repair. **Decided by** ADR 0100, 0126, 0176, 0181, 0207, 0221, 02
`package` on `node-package-manager`, a `container` on `node-container-runtime` `package` on `node-package-manager`, a `container` on `node-container-runtime`
(ADR 0207). (ADR 0207).
Not a claim: a module claims a seat it holds and declares resources. Not a claim: a module claims a seat it holds and declares resources.
- **declaration** — what the controller sends one node: every resource of every module assigned there, - **declaration** — every resource of every module assigned to one node, composed, which the controller sent
composed. The noun is this domain's; what a manifest says is that it *declares* (Module), and a manifest that node until release 6 of ADR 0429. From release 6 nothing is sent: what a node applies is its
is never called a declaration. The controller's verb `plan` previews a node's declaration. compositions, and the node-engine keeps the declaration it applied last only as what it starts from, its
- **send** — the controller giving a node its declaration. The controller's verb `push` asks for a **last declaration** (ADR 0539 §9). The switch's own last declaration carries the operator's public key
send (of one node, or of every node); prose says send. A git push and a phone's push notification are (ADR 0539 §3, §11). The noun is this domain's; what a manifest says is that it *declares* (Module), and a
other things (see [Homonyms](#homonyms)). manifest is never called a declaration. The controller's verb `plan` previewed a node's declaration until
- **apply** — the node-engine making its node match its declaration; **reconcile** is doing so again release 6.
until nothing differs. - **composition** — one module's resources composed for one node at one fingerprint, signed by the controller
(or, made by hand with `mesh-hand-compose`, by the operator), written by the controller as
`<node>.<module>.<fingerprint>` in that node's bucket `composed-<node>`, which only that node reads; the node's
own resources (adoption, firewall openings, accounts, the resolver, identities and groups) are its node
composition `<node>.node`. Its digest, with the controller's judgement of the move from what the node runs
now, is kept under the same key in `mesh-controller_composed`; a want names that digest. Derived state, kept
while its fingerprint is among the module's five newest or a want names it, and no link of the chain
(ADR 0539 §1 to §4). From release 6. Not mesh-sdk's composition, the function that composes it (see
[Homonyms](#homonyms)), nor the **composition check** (Delivery).
- **send** — the controller giving a node its declaration, until release 6 of ADR 0429. The controller's verb
`push` asked for a send (of one node, or of every node); prose says send. From release 6 nothing is sent: the
controller writes compositions and the assigner writes wants, and every act that sent writes compositions
instead (ADR 0539 §5). A git push and a phone's push notification are other things (see
[Homonyms](#homonyms)).
- **apply** — the node-engine making its node match what it should run. From release 6 of ADR 0429 its node
agent applies one module at a time, the composition whose digest the module's want names, only when the
want's fingerprint or digest differs from what runs, and removes only that module's own resources
(ADR 0539 §9); until then it applied the node's whole declaration. **reconcile** is doing so again until
nothing differs; from release 6 a failed apply is retried by the next change of the want or the composition,
never by a clock.
- **operator account** — the machine account of the operator's person identity on a node, read from the - **operator account** — the machine account of the operator's person identity on a node, read from the
identity and no longer stated on the node record; empty for a node nobody logs into. Everything the mesh identity and no longer stated on the node record; empty for a node nobody logs into. Everything the mesh
places under a person's home is resolved against this account's home and owned by it places under a person's home is resolved against this account's home and owned by it
@@ -344,7 +389,7 @@ and vault (Identity and access).
**Purpose.** To issue, hold, rotate and check the credentials and grants by which modules, nodes, agents **Purpose.** To issue, hold, rotate and check the credentials and grants by which modules, nodes, agents
and the operator act. **Recorded by** the controller's `identity` and `licences` databases and the vault. and the operator act. **Recorded by** the controller's `identity` and `licences` databases and the vault.
**Upstream of** Provisioning, Delivery, and Operator and conversation. **Upstream of** Provisioning, Delivery, and Operator and conversation.
**Decided by** ADR 0085, 0113, 0183, 0225, 0234, 0386, 0387, 0388, 0411, 0413, 0414. **Decided by** ADR 0085, 0113, 0183, 0225, 0234, 0386, 0387, 0388, 0411, 0413, 0414, 0550.
**Uses:** module, node, seat, tool (Module), operator, consumer and provision (Provisioning), agent and ask (Operator and conversation). **Uses:** module, node, seat, tool (Module), operator, consumer and provision (Provisioning), agent and ask (Operator and conversation).
- **credential** — anything that proves an identity: a password, a key, a token. A **pair credential** - **credential** — anything that proves an identity: a password, a key, a token. A **pair credential**
@@ -356,7 +401,8 @@ and the operator act. **Recorded by** the controller's `identity` and `licences`
and never made by the mesh and never made by the mesh
(ADR 0283). (ADR 0283).
- **vault** — the module that owns every secret (`mesh-vault`, - **vault** — the module that owns every secret (`mesh-vault`,
[24 — The secrets vault](../03-DESIGN/01-to-be/24-the-secrets-vault.md)). [24 — The secrets vault](../03-DESIGN/01-to-be/24-the-secrets-vault.md)): the mesh's signing key, with which
it signs on request, licence grants and tokens, bus account passwords and module secrets (ADR 0550).
- **identity** — a person, an agent or a module as the mesh knows it: a name, a kind, a role, and whether it - **identity** — a person, an agent or a module as the mesh knows it: a name, a kind, a role, and whether it
is switched on; a person or an agent also has a login name, the machines it is present on and, for a person, is switched on; a person or an agent also has a login name, the machines it is present on and, for a person,
the login kept on a machine where it differs. Every identity and role is held by the identity seat, in the the login kept on a machine where it differs. Every identity and role is held by the identity seat, in the
@@ -403,29 +449,56 @@ 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 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 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. 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, 0524–0530, 0539, 0548, 0551.
**Uses:** module, node, declaration and send (Placement), health (Health and repair), person. **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** - **merge check** — what a pull request carries before it may merge, judging the merge of its head into a
(`mesh/merge-gate`, the build seat's composed check of the modules the change touches) and the named **trunk commit**, made in a clone of its own on a build node and never pushed (ADR 0323). One status,
**repository check** (`mesh/repo-check`, the repository's own `merge-check.sh`) `mesh/check`, written by the checker: the change's tests, run by the language's build module, and the
(ADR 0238). Both judge the merge of the pull request's head into a named **trunk commit**, made in the **composition check**; for a delivery group, one verdict on every member's head (ADR 0445, ADR 0447). Before
build seat's own clone and never pushed (ADR 0323). release 4 of ADR 0429 a pull request carried two statuses, one for the modules the change touches and one
for the repository's own script (ADR 0238).
- **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 mesh-sdk's composition, the one the controller
composes with
(ADR 0446). It replaces the controller's check of that kind, which composed every machine from release 4 of
ADR 0429 back (issue 1303).
*Not:* ~~merge gate~~ (hq)
- **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).
- **carried verdict** — a delivery's verdict moved to a newer trunk commit, or a newer sibling commit, by its
package digests: the checker merges the head onto the new commit and runs only the packages whose digest the
move changed, keeping the composition while no composed fingerprint changed; the verdict names the commits it
was carried from (ADR 0548 §5, §6). A head git refuses onto the new commit is not carried: its next check is
whole (ADR 0548 §7).
- **trunk commit** — the commit of the trunk a merge check's verdict was judged against: the trunk's head when - **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 the build module cloned it. The verdict holds for that commit only, goes out of date when the trunk moves, and
(ADR 0323). is then carried to the new commit (ADR 0323, ADR 0548).
- **build seat** — the seat whose holder builds modules, `node-build-agent`; its holder on a node is the - **merge turn** — an approved merge's key on a repository in the gate's `turns` bucket: the deliveries waiting
**builder**. Any node may hold the seat, so there is no build node by nature to merge there in the order of their Approve, and the check each waits for. It holds new checks off that
(ADR 0237). repository until the merge is done (ADR 0548 §3). Written only while the check gates merges (the gate's
setting `check-gates-merge`); while it does not, no turn is written and an approved merge goes in at once
(ADR 0551 §1).
- **build seat** — a language's node seat, held on a node by that language's build module: `node-build-go`,
`node-build-node` and `node-build-image` (ADR 0525 §2). Its accepted kinds are the **build streams**, and its
verbs `current`, `pause` and `resume` serve that node's pulls. It replaces the seat `node-build-agent`, retired
in R4g (ADR 0525 §10). Any node may hold it, and a node whose **build capacity** is above zero builds, so there is
no build node by nature (ADR 0237).
*Not:* ~~build machine~~ *Not:* ~~build machine~~
- **build queue** — the builds waiting to run, kept by the bus as three work queues a builder takes in order: - **build queue** — the build requests waiting to run, kept by the bus alone: per language, the work-queue streams
delivery builds, then checks of pull requests whose Approve is asked or given (ADR 0421 §10), then every other check; each check queue keeps one `delivery`, `check` and `recheck`, which a build module pulls from, deliveries first, while its node has a free
request per pull request, the newest (ADR 0406); one entry in it is a slot of the build capacity (ADR 0525 §1, §4; ADR 0536 §4). No part keeps a list of them: the controller's queue
**build request**. and its verbs are gone, and a stream's backlog and the age of its oldest request are read from its consumers
The queue's verbs still say *ask* for an entry; the conversation's ask is another thing (ADR 0525 §9). One entry in it is a **build request**, an ask on a build stream for one whole job on one node;
(see [Homonyms](#homonyms)). the conversation's ask is another thing (see [Homonyms](#homonyms)).
- **build capacity** — how many build requests a node's build modules hold at once: a setting of the node that the
build modules read, and 0 pulls nothing (ADR 0525 §5). Not a seat's capacity (**capacity**, Module).
- **build module** — the module of one language that does the build steps for it on a node (fetch, build, digest,
test), claiming that language's build seat: `mesh-build-go`, `mesh-build-node` and `mesh-build-image`. It pulls the
build requests of its language and writes each step's end on its own key (ADR 0525 §3, §6). Not the **builder**.
- **package** — what code resolves when it is **compiled**: an npm/cargo/pypi dependency, by **version**. - **package** — what code resolves when it is **compiled**: an npm/cargo/pypi dependency, by **version**.
Served by the **package-registry** (gitea). Only a builder talks to it. Served by the **package-registry** (gitea). Only a build module talks to it.
- **artifact** — anything a build produces and the mesh delivers to a node by **digest**: an `image`, a - **artifact** — anything a build produces and the mesh delivers to a node by **digest**: an `image`, a
mirrored `upstream` image, a `bundle` of the module's own code, an `archive`. Served by the mirrored `upstream` image, a `bundle` of the module's own code, an `archive`. Served by the
**artifact store**, an OCI registry that holds every kind as content-addressed blobs **artifact store**, an OCI registry that holds every kind as content-addressed blobs
@@ -441,9 +514,9 @@ controller (the planner, the rollout) until release 6 of ADR 0429; from each rel
is built from; this entry narrows it to the files within, and that use reads as *the repository holding is built from; this entry narrows it to the files within, and that use reads as *the repository holding
the build source*. Sharing a repository is not a dependency, and no build source is a whole repository the build source*. Sharing a repository is not a dependency, and no build source is a whole repository
holding another module's files. holding another module's files.
- **catalogue** — what the mesh holds of every module, at which commit; and the repository the - **catalog** — what the mesh holds of every module, at which commit; and the repository the
modules live in. `mesh-catalog` and `catalog_modules` are identifiers and keep their spelling. modules live in. `mesh-catalog` and `catalog_modules` are identifiers and keep their spelling.
*Not:* ~~catalog~~ (hq) *Not:* ~~catalogue~~
- **delivery** — one commit in one repository on its way to the nodes, from its pull request's head - **delivery** — one commit in one repository on its way to the nodes, from its pull request's head
being announced to one of its final states: one pull request, one status, one note being announced to one of its final states: one pull request, one status, one note
(ADR 0239). (ADR 0239).
@@ -452,14 +525,17 @@ controller (the planner, the rollout) until release 6 of ADR 0429; from each rel
Its states are one table (`proposed`, `checked`, `ready` or `rejected`, `published`, `delivering`, Its states are one table (`proposed`, `checked`, `ready` or `rejected`, `published`, `delivering`,
`held`, and the final six: `delivered`, `failed`, `superseded`, `stopped`, `retired` and `covered`; `held`, and the final six: `delivered`, `failed`, `superseded`, `stopped`, `retired` and `covered`;
ADR 0250, ADR 0250,
ADR 0270). Not *change* (a diff). *Pipeline* is the predecessor's build-and-deploy ADR 0270). From release 6 of ADR 0429 the gate keeps the merge's states alone (`proposed`, `ready`,
`rejected`, `held`, `merged` and `closed`), and a merged delivery is delivered when its source revision is,
derived from the wants and the haves (ADR 0539 §17). Not *change* (a diff). *Pipeline* is the predecessor's build-and-deploy
mechanism, which the as-is designs describe; it is never a word for a delivery. mechanism, which the as-is designs describe; it is never a word for a delivery.
*Not:* ~~deployment~~ (hq) *Not:* ~~deployment~~ (hq)
- **covered** — the end of a delivery that a later merge containing it delivered: every module it moves was - **covered** — the end of a delivery that a later merge containing it delivered: every module it moves was
sent to every machine running it by the rollout of a later merge into the same trunk, delivered on its own sent to every machine running it by the rollout of a later merge into the same trunk, delivered on its own
passing check, whose merge commit the forge says contains this one. Not *superseded*, which is a delivery passing check, whose merge commit the forge says contains this one. Not *superseded*, which is a delivery
whose rollout a newer delivery took over before it ended; nor *delivered*, which its own rollout or an empty plan whose rollout a newer delivery took over before it ended; nor *delivered*, which its own rollout or an empty plan
ends in (ADR 0270). ends in (ADR 0270). As a state of the gate it goes at release 6; a newer delivered source revision then
covers the older merges of its repository (ADR 0539 §17).
- **delivery group** — two or more deliveries sharing a pull request head branch name across the mesh's - **delivery group** — two or more deliveries sharing a pull request head branch name across the mesh's
repositories, delivered as one unit in an order declared (`after:`) or inferred by the planner. One repositories, delivered as one unit in an order declared (`after:`) or inferred by the planner. One
level: a group holds deliveries, never groups. Its state is derived from its members, never set. level: a group holds deliveries, never groups. Its state is derived from its members, never set.
@@ -469,41 +545,47 @@ controller (the planner, the rollout) until release 6 of ADR 0429; from each rel
lone delivery (ADR 0401). Work is never held back on a branch other than a trunk: the trunk is what runs. lone delivery (ADR 0401). Work is never held back on a branch other than a trunk: the trunk is what runs.
*Not:* ~~release branch~~ (hq) *Not:* ~~release branch~~ (hq)
- **toolchain** — a language's compiler and tools, installed by that language's build module on every node holding - **toolchain** — a language's compiler and tools, installed by that language's build module on every node holding
the build seat, pinned to one version that is the same on every builder; its version is part of a build's reuse its build seat, pinned to one version that is the same on every build node; its version is part of a build's reuse
key (ADR 0405). No longer an image. key (ADR 0405). No longer an image.
- **delivery plan** — what a delivery does to the mesh: its **build plan** (the modules it moves and - **delivery plan** — what a delivery does to the mesh: its **build plan** (the modules it moves and
their dependents, in tiers), its **deploy plan** (per node, what it receives and what waits for a their dependents, in tiers), its **deploy plan** (per node, what it receives and what waits for a
person) and its verdict (the composed nodes, the replays). Computed by the controller's planner from a person) and its verdict (the composed nodes, the replays). Computed by the controller's planner from a
diffset; the controller's verb `delivery-plan` shows it. *Deploy* is used in this one place. Deleted at diffset; the controller's verb `delivery-plan` showed it. *Deploy* is used in this one place. Deleted at
release 6 of ADR 0429 (§8): the word stays in use until that release is delivered, and then retires. release 6 of ADR 0429 (§8, ADR 0539): the word stays in use until that release is delivered, and goes to a
*Not:* line with the as-is update that follows the delivery (playbook 02).
*Not:* ~~change plan~~, ~~release plan~~ *Not:* ~~change plan~~, ~~release plan~~
- **merge window** — the rolling wait in which the controller collects merges before it plans them: each - **merge window** — the rolling wait in which the controller collects merges before it plans them: each
merge restarts it, and it closes when none arrived for its length (90 seconds by default) or at the latest merge restarts it, and it closes when none arrived for its length (90 seconds by default) or at the latest
10 minutes after its first merge (ADR 0276). Kept as two keys with a time to live on the bus; the first to 10 minutes after its first merge (ADR 0276). Kept as two keys with a time to live on the bus; the first to
expire closes the batch (ADR 0407). Goes at release 6 of ADR 0429, when nothing collects merges (§12). expire closes the batch (ADR 0407). Goes at release 6 of ADR 0429, when nothing collects merges (§12), and
its keys' bucket is deleted (ADR 0539 §13); the word goes to a *Not:* line with the as-is update after that
delivery.
- **batch** — the merges one merge window collected, planned once as one rollout. For a repository merged more - **batch** — the merges one merge window collected, planned once as one rollout. For a repository merged more
than once in it, the rollout takes the latest commit, and the earlier merges are answered by it. Not *group*, than once in it, the rollout takes the latest commit, and the earlier merges are answered by it. Not *group*,
which is a delivery group. Goes at release 6 of ADR 0429 (§11, §12). which is a delivery group. Goes at release 6 of ADR 0429 (§11, §12): a merge moves the source revision and
nothing collects it; the word goes to a *Not:* line with the as-is update after that delivery.
- **mesh-delivery** — the repository of the delivery domain, holding its parts as modules prefixed - **mesh-delivery** — the repository of the delivery domain, holding its parts as modules prefixed
`mesh-delivery-` (ADR 0429 §2), made by the forge module from its config (ADR 0428). Until release 3 of `mesh-delivery-` (ADR 0429 §2), made by the forge module from its config (ADR 0428). Until release 3 of
ADR 0429 also the module in the catalogue that owns deliveries and delivery groups, holding the mesh-scoped ADR 0429 also the module in the catalog that owns deliveries and delivery groups, holding the mesh-scoped
seat of the same name: it records and decides, and the controller sends, judges and rolls back when it is seat of the same name: it records and decides, and the controller sends, judges and rolls back when it is
asked. Release 3 splits that module into the parts, and then the name means the repository alone. asked. Release 3 splits that module into the parts, and then the name means the repository alone.
- **part** — one independent process of the delivery domain with its own bus credential, which watches keys - **part** — one independent process of the delivery domain with its own bus credential, which watches keys
and writes keys and never calls another part (ADR 0429 §1). Each key has one writer; the operator's and writes keys and never calls another part (ADR 0429 §1). Each key has one writer; the operator's
credential may also write a want, the one manual escape. The parts: the **forge bridge** (forge events in, credential may also write a want, the one manual escape. The parts: the **forge bridge** (forge events in,
the only caller of the forge), the **indexer** (fingerprints, the candidates of a group, the modules of the the only caller of the forge), the **indexer** (fingerprints, the candidates of a group, the modules of the
trunk), the **builder** (claims and builds one fingerprint once), the **checker** (the merge check on built trunk), the **builder** (claims one fingerprint once and orders its build), the **checker** (the merge check on built
artifacts), the **gate** (merges a group and moves the source revision; *gate part* where it could be read artifacts), the **gate** (merges a group and moves the source revision; *gate part* where it could be read
as another gate, see [Homonyms](#homonyms)), the **teller** (the operator's phone, and the only writer of an as another gate, see [Homonyms](#homonyms)), the **teller** (the operator's phone, and the only writer of an
ask's answer), the **assigner** (writes each node's wants by its six rules), the **node agent** (the ask's answer), the **assigner** (writes each node's wants by its seven rules, ADR 0539 §6), the **node
node-engine's own process, applying its wants), the **watchman** (presence and pulses) and the **status agent** (the node-engine's own process, applying its wants), the **watchman** (presence and pulses; it also holds the
view** (`mesh-delivery-cli` and a web view, writing only its answers to asks). The reviewer is the review Health seat `mesh-watchdogs`, whose watchdogs are pulses, and keeps the dead letters, ADR 0564 §4, §5) and the
**status view** (`mesh-delivery-cli` and a web view, writing only its answers to asks and saying the events
`delivered`, `moving`, `settled` and `stuck`, derived from the keys). The reviewer is the review
agent's session, not a part. Built over releases 1 to 6 of ADR 0429. agent's session, not a part. Built over releases 1 to 6 of ADR 0429.
- **fingerprint** — what identifies a module's build by its content: a hash over its source files, its - **fingerprint** — what identifies a module's build by its content: a hash over its source files, its
manifest and the fingerprints of its build-time inputs, so equal content gives an equal fingerprint whatever manifest and the fingerprints of its build-time inputs, so equal content gives an equal fingerprint whatever
the commit (ADR 0429 §5). It is today's source fingerprint with a base entering by its module's the commit (ADR 0429 §5). It is today's source fingerprint with a base entering by its module's
fingerprint, the builder by its own module's fingerprint, and a build resolving packages by its lock. From fingerprint, and a build resolving packages by its lock; from R4g the builder is no input of any fingerprint (ADR 0530). From
release 4 a build's key, so one source is never built twice. release 4 a build's key, so one source is never built twice.
- **source revision** — one revision of the key `source.main`, written only by the gate: every repository's - **source revision** — one revision of the key `source.main`, written only by the gate: every repository's
merged head and the group that moved it. A group reaches the mesh as one source revision (ADR 0429 §11); a merged head and the group that moved it. A group reaches the mesh as one source revision (ADR 0429 §11); a
@@ -511,11 +593,15 @@ controller (the planner, the rollout) until release 6 of ADR 0429; from each rel
want and is healthy, derived from the keys and recorded by no part (§1). want and is healthy, derived from the keys and recorded by no part (§1).
- **canary** — the first node of a module's nodes, ordered by ring then name: a new fingerprint reaches it - **canary** — the first node of a module's nodes, ordered by ring then name: a new fingerprint reaches it
first and the module's other nodes only once it runs healthy there (ADR 0429 §6). Replaces sending to every first and the module's other nodes only once it runs healthy there (ADR 0429 §6). Replaces sending to every
machine at once from release 6 (§15). machine at once from release 6 (§15); a fingerprint that fails there is held and the canary set back, or for
- **want** and **have** — a want (`want.<node>.<module>`) is the fingerprint a node should run, written by the a module that keeps data the operator is asked (ADR 0539 §18, §19).
assigner; a have (`have.<node>.<module>`) is what the node runs, its phase and health, written by that node's - **want** and **have** — a want (`want.<node>.<module>`) is the fingerprint a node should run of a module and
agent (ADR 0429 §4, §6). A node's facts that no module owns are its node key. They replace assignments and the digest of the one **composition** it allows for it, written by the assigner by its seven rules; the node's
rollouts at release 6 (§17); haves are written from release 1. own want `want.<node>.node` names its node composition (ADR 0429 §4, §6; ADR 0539 §1, §6). A want may also
carry the operator's answer to a failed backup (ADR 0539 §16). A have (`have.<node>.<module>`) is what the
node runs: the fingerprint, the composition's digest, its phase (`held` among them) and health, written by
that node's agent. A node's facts that no module owns are its node key. They replace rollouts at release 6,
and the assignments keep placement alone (ADR 0429 §17, ADR 0539 §12); haves are written from release 1.
- **pulse** — a key the watchman writes for each key whose next move a machine owes, with a time to live; its - **pulse** — a key the watchman writes for each key whose next move a machine owes, with a time to live; its
expiry is a stall, raised as a condition, so no part needs a timer (ADR 0429 §1, research 063). A person's expiry is a stall, raised as a condition, so no part needs a timer (ADR 0429 §1, research 063). A person's
wait carries none. wait carries none.
@@ -525,10 +611,13 @@ controller (the planner, the rollout) until release 6 of ADR 0429; from each rel
merge it answers, and its record is one key of the bus's rollouts bucket, a link of the chain (ADR 0408). A primitive the delivering stage merge it answers, and its record is one key of the bus's rollouts bucket, a link of the chain (ADR 0408). A primitive the delivering stage
asks for, not an object anyone manages. A **tier** is one step of a rollout: a module, then the modules asks for, not an object anyone manages. A **tier** is one step of a rollout: a module, then the modules
built against it. The controller's verb `plans` lists the rollouts. A module is rolled out by its `roll` built against it. The controller's verb `plans` lists the rollouts. A module is rolled out by its `roll`
policy. Rollouts and tiers go at release 6 of ADR 0429 (§8, §15), replaced by wants and haves (§17). policy. Rollouts and tiers go at release 6 of ADR 0429 (§8, §15), replaced by wants and haves (§17); the
rollouts bucket is kept read-only with its history (ADR 0539 §12), and both words go to a *Not:* line with
the as-is update after that delivery.
*Not:* ~~walk~~ (hq) *Not:* ~~walk~~ (hq)
- **first-node gate** — the judgement on a delivery's first node: healthy three times, no witness put it - **first-node gate** — the judgement on a delivery's first node: healthy three times, no witness put it
back, no condition raised since the send (ADR 0236 §2). Not *the gate* bare (see [Homonyms](#homonyms)). back, no condition raised since the send (ADR 0236 §2). Goes at release 6 of ADR 0429, replaced by the
**canary**. Not *the gate* bare (see [Homonyms](#homonyms)).
- **core module** and **leaf module** — a core module is one on the controller's own path (the controller, - **core module** and **leaf module** — a core module is one on the controller's own path (the controller,
the node-engine, the tool runner, the bus, mesh-delivery) or the mesh's resolver, whose judgement protects the node-engine, the tool runner, the bus, mesh-delivery) or the mesh's resolver, whose judgement protects
the mesh's ability to reach a node at all; a leaf module is every other module. Decided by the mesh's ability to reach a node at all; a leaf module is every other module. Decided by
@@ -544,8 +633,9 @@ controller (the planner, the rollout) until release 6 of ADR 0429; from each rel
part. Its **fix budget** is what it is held to: five minutes for a leaf module, ten for a core module, counted part. Its **fix budget** is what it is held to: five minutes for a leaf module, ten for a core module, counted
from the move to `fixing`; an issue whose fixes move no module has none (ADR 0313). Not the delivery time, from the move to `fixing`; an issue whose fixes move no module has none (ADR 0313). Not the delivery time,
which starts at the merge. which starts at the merge.
- **release** — a person's word that a held delivery goes on; the verb `mesh-delivery.release`. Used in - **release** — a person's word that a held delivery goes on; the verb `mesh-delivery.release`, until release 6
this sense only. of ADR 0429, when the verb goes (ADR 0539 §17) and a held move goes on by the operator's answer to the
assigner's ask. Used in this sense only.
- **rollback** — putting a node back on the build it ran before, by something other than the build being - **rollback** — putting a node back on the build it ran before, by something other than the build being
judged (a witness, ADR 0236). judged (a witness, ADR 0236).
- **the lab**, **scenario**, **replay** — a real mesh raised to run a change against before it reaches - **the lab**, **scenario**, **replay** — a real mesh raised to run a change against before it reaches
@@ -556,7 +646,7 @@ controller (the planner, the rollout) until release 6 of ADR 0429; from each rel
**Purpose.** To notice what is wrong with the mesh, say it once, repair what may be repaired unattended, **Purpose.** To notice what is wrong with the mesh, say it once, repair what may be repaired unattended,
and record what was done by hand. **Recorded by** the controller's condition store. and record what was done by hand. **Recorded by** the controller's condition store.
**Upstream of** Delivery (the first-node gate) and Operator and conversation (a condition **Upstream of** Delivery (the first-node gate) and Operator and conversation (a condition
becomes a message). **Decided by** ADR 0227, 0231, 0240, 0252, 0283. becomes a message). **Decided by** ADR 0227, 0231, 0240, 0252, 0283, 0564.
**Uses:** node, module, node-engine (Core). **Uses:** node, module, node-engine (Core).
- **health** — a module's or a core component's statement of how it is alive and ready, which the - **health** — a module's or a core component's statement of how it is alive and ready, which the
@@ -570,7 +660,14 @@ becomes a message). **Decided by** ADR 0227, 0231, 0240, 0252, 0283.
reads it as *waits for a person* reads it as *waits for a person*
(ADR 0283). (ADR 0283).
- **probe** — one look at one thing's health, run by whoever owns the verdict; what a probe returns is a - **probe** — one look at one thing's health, run by whoever owns the verdict; what a probe returns is a
**finding**, before it becomes a condition. **finding**, before it becomes a condition. A finding is kept as a key `<scope>.<id>.<kind>` in its observer's
bucket `findings` until it is no longer observed (ADR 0564 §2).
- **observer** — a part that writes findings for the condition keeper: the watchman, the self-check and the
healers (ADR 0564 §2). An observer never writes a condition.
- **condition keeper** — the module mesh-conditions, holder of the seat `mesh-conditions`: the one writer of the
open conditions, raising each from an observer's finding and clearing it when the finding goes, and saying
`condition-raised`, `condition-changed` and `condition-cleared` (ADR 0564 §2, §3). The controller keeps them
until the take-over.
- **signal** and **watchdog** — something that must keep happening (a heartbeat, a report after a send), - **signal** and **watchdog** — something that must keep happening (a heartbeat, a report after a send),
and the watcher that raises a condition when it stops (to-be 45 §3). and the watcher that raises a condition when it stops (to-be 45 §3).
- **condition** — one open fact about something the mesh owns that is wrong, with a key and a severity; - **condition** — one open fact about something the mesh owns that is wrong, with a key and a severity;
@@ -578,8 +675,8 @@ becomes a message). **Decided by** ADR 0227, 0231, 0240, 0252, 0283.
(ADR 0231). (ADR 0231).
What a wrapped dashboard raises is its own; the mesh's notion is a condition. What a wrapped dashboard raises is its own; the mesh's notion is a condition.
*Not:* ~~alert~~ (hq) *Not:* ~~alert~~ (hq)
- **self-check** — the controller's own examination of the mesh, run on demand and on a schedule - **self-check** — the mesh's own examination of itself, run on demand and on a schedule (to-be 45 §4), held
(to-be 45 §4). Its verb is `doctor`, an identifier; prose says self-check. by the seat `mesh-self-check` (ADR 0564 §6); the controller runs it until that is delivered. Its verb is `doctor`, an identifier; prose says self-check.
*Not:* ~~doctor~~ (hq) *Not:* ~~doctor~~ (hq)
- **healer** — a registered response to one condition kind: its **repair** (the ordinary path again), - **healer** — a registered response to one condition kind: its **repair** (the ordinary path again),
its **budget**, its back-off and its brake. A healer may not withdraw, delete or recreate data. its **budget**, its back-off and its brake. A healer may not withdraw, delete or recreate data.
@@ -589,7 +686,15 @@ becomes a message). **Decided by** ADR 0227, 0231, 0240, 0252, 0283.
channel, to see an answer travel the whole way: its approval performs nothing and is recorded as the channel, to see an answer travel the whole way: its approval performs nothing and is recorded as the
operator's decision (ADR 0259). operator's decision (ADR 0259).
Not a drill: nothing is broken. Not a drill: nothing is broken.
- **hand-act** — a repair a person made by hand, recorded so the mesh knows it happened. - **hand-act** — a repair a person made by hand, recorded so the mesh knows it happened. The **hand-act log**
is the stream `acts` of the seat `mesh-hand-acts`, held once by the module mesh-hand-acts, which also records
who chose an ask (`warranted`); every recorder calls it, and the controller's own log is read beside it
until the lease write (ADR 0565 §10, §11).
- **take-over** — the operator's approved switch of a job from the controller to the part that replaces it, with a
way back until the lease write: for the bus's user list, the grants part's list becomes the one loaded (ADR 0547
§8); for conditions, the condition keeper's events become the ones the router hears (ADR 0564 §9); for
config, mesh-config's `take-over` is refused while its `compare` shows a difference between its new source and
the controller's mirrors (ADR 0565 §4).
- **witness** — what rolls a core component back when its new build does not become healthy: never the - **witness** — what rolls a core component back when its new build does not become healthy: never the
component itself (to-be 45 §8). component itself (to-be 45 §8).
@@ -664,7 +769,7 @@ asking, never the act. **Decided by** ADR 0152, 0175, 0234, 0259, 0272, 0383, 03
say `mesh-cli`. The operator also calls the mesh itself *nox mesh*. *nox* is also the agent (ADR 0414 §7): see say `mesh-cli`. The operator also calls the mesh itself *nox mesh*. *nox* is also the agent (ADR 0414 §7): see
[Homonyms](#homonyms), and say which is meant. It is the one command the operator types: every line the mesh addresses to the operator — a [Homonyms](#homonyms), and say which is meant. It is the one command the operator types: every line the mesh addresses to the operator — a
condition's words, a verb's answer, a README, a design — names a `nox` line, never a daemon's (the controller's condition's words, a verb's answer, a README, a design — names a `nox` line, never a daemon's (the controller's
or the node-engine's binary followed by a command), which `words.py` and the catalogue's check refuse or the node-engine's binary followed by a command), which the words test and the catalog's check refuse
(ADR 0272, (ADR 0272,
its insight of 2026-10-09). its insight of 2026-10-09).
- **channel / intake** — the two kinded benches the mesh talks to its operator through: `channel` - **channel / intake** — the two kinded benches the mesh talks to its operator through: `channel`
@@ -703,8 +808,8 @@ asking, never the act. **Decided by** ADR 0152, 0175, 0234, 0259, 0272, 0383, 03
- **reference** — an opaque token in a message's words standing for a detail the content rule keeps out - **reference** — an opaque token in a message's words standing for a detail the content rule keeps out
of them (a path, an address); opened with `detail` only on a `private` channel or through the mesh MCP server. of them (a path, an address); opened with `detail` only on a `private` channel or through the mesh MCP server.
- **agent** — a coding agent: the operator's, or one the mesh runs; to the mesh, an identity of kind agent - **agent** — a coding agent: the operator's, or one the mesh runs; to the mesh, an identity of kind agent
(ADR 0386). Not the node-engine, and not the (ADR 0386). Not the node-engine, and not a
build seat's holder, whose seat name `node-build-agent` is an identifier to rename. build module.
- **thread** — one line of agent work, owned by the mesh: its turns, tool calls and results, its workspace - **thread** — one line of agent work, owned by the mesh: its turns, tool calls and results, its workspace
and usage, kept whole for good on the bus by the thread seat; it outlives any session, node or front end and usage, kept whole for good on the bus by the thread seat; it outlives any session, node or front end
(ADR 0383, ADR 0384). (ADR 0383, ADR 0384).
@@ -740,7 +845,7 @@ it was told, and the servarr apps' own libraries. **Upstream of** nothing. **Dec
**Uses:** module, seat, config value (Config). **Uses:** module, seat, config value (Config).
- **content** — a work a person watches, listens to or reads: a film, an album and its tracks, an - **content** — a work a person watches, listens to or reads: a film, an album and its tracks, an
episode, a book, a session of a sport event. It is known by an identity from a catalogue outside the episode, a book, a session of a sport event. It is known by an identity from a catalog outside the
mesh, never by a file's name, and a work with no copy anywhere is still content (ADR 0302). mesh, never by a file's name, and a work with no copy anywhere is still content (ADR 0302).
- **kind** — a kind of content, declared as configuration, never code: its parts, the metadata fields - **kind** — a kind of content, declared as configuration, never code: its parts, the metadata fields
that identify a work, the file extensions of its copies, and its folder and file templates. The first that identify a work, the file extensions of its copies, and its folder and file templates. The first
@@ -763,6 +868,75 @@ 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 - **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. 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; and its handling, action log and suggested actions, ADR 0517, ADR 0519). **Downstream of** Identity and access and Operator and conversation; **upstream of** nothing.
**Decided by** ADR 0359, 0361, 0378, 0439, 0440, 0458, 0459, 0461, 0462, 0472, 0515 to 0519.
**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). Any caller `mesh-mail`'s `grants` names for that
account may ask for one (ADR 0472).
- **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 *catalog entry*: the catalog 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 operator's list** — what the mail page opens on: the tasks by their due date, the promises, *good to know*,
*read later* and *handled today*, for one mode (ADR 0515 §1). Always *the operator's list*: the bare *list* is
the classical list of mail, shown as *All mail*.
- **task** — what a mail asks the operator to do, with a due date or none, written by mail reading in its own words
(ADR 0516 §1).
- **promise** — what a mail says someone will do: *you promised*, *waiting on others* or *on its way*, with who and
when (ADR 0516 §1).
- **read later** — the flag of a mail worth reading that asks nothing, which a mail of any mark may carry (ADR
0516 §1). Not a *mark*.
- **handled** — what the operator marked done on the operator's list; it changes nothing in the mailbox (ADR 0517
§1).
- **action log** — mail reading's record of every act of the operator on the mail page, never mail text, kept a
year (ADR 0517 §3).
- **Owly** — the mail page's assistant: it answers mail questions through mail reading, gives the morning briefing,
and suggests actions; it sends and changes nothing by itself (ADR 0515 §2, ADR 0518).
- **morning briefing** — Owly's account of the day, asked the first time the mail page opens each day (ADR 0518).
- **suggested action** — an *archive*, *report as phishing* or *unsubscribe* that mail reading puts forward for the
operator's Approve on the mail page (ADR 0519 §1). Not *proposal* (Operator and conversation's, which takes effect
on a warrant).
- **approval inbox** — the place on the mail page where the suggested actions wait, each with *Approve* and *Not
now* (ADR 0519 §4).
## The record — how this repository works ## 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. Not a domain of the mesh but of the way it is built, listed because its words meet the mesh's.
@@ -792,6 +966,10 @@ Not a domain of the mesh but of the way it is built, listed because its words me
- **design state** — of a design: proposed, designed, in progress or implemented, worked out by the knowledge base - **design state** — of a design: proposed, designed, in progress or implemented, worked out by the knowledge base
from the issues with a design link to it and whether their deliveries are done; never written by anyone from the issues with a design link to it and whether their deliveries are done; never written by anyone
(ADR 0424). (ADR 0424).
- **progress** — of a design, a milestone, an issue or a record: how far it has come, each part done, moving,
waiting or stuck with when it last moved, worked out by the knowledge base's verb `progress` from the issue tracker
and the deliveries and never written by anyone; `minimal` says one line per part, `detailed` the tree of a lead's
report (issue 1947).
- **glossary entry** — one entry of the glossary as a record of the knowledge base: its head word or words, its - **glossary entry** — one entry of the glossary as a record of the knowledge base: its head word or words, its
domain, its meaning, its *Not:* line and its identifier line (ADR 0423). Until the move, an entry of this page. domain, its meaning, its *Not:* line and its identifier line (ADR 0423). Until the move, an entry of this page.
- **milestone** — a group of a design's issues delivered together, as one delivery group, or one pull request when - **milestone** — a group of a design's issues delivered together, as one delivery group, or one pull request when
@@ -827,24 +1005,27 @@ Not a domain of the mesh but of the way it is built, listed because its words me
- **tracks** — of an issue and a condition: the issue links the condition's key with a link of kind `condition`. The - **tracks** — of an issue and a condition: the issue links the condition's key with a link of kind `condition`. The
**tracking line** is the line the router says in every message about a condition: the issues that track it and **tracking line** is the line the router says in every message about a condition: the issues that track it and
where each stands, or that none does (ADR 0285). where each stands, or that none does (ADR 0285).
- **hq check** — one of `00-META/checks/`, run by `merge-check.sh` as this repository's check. - **hq check** — one of the Go tests in `00-META/checks/`, run by `go test ./...` and by the checker as `mesh/check`.
## Homonyms ## Homonyms
A word below means different things in different domains. In a governing document it is never bare; A word below means different things in different domains. In a governing document it is never bare;
it is always the qualified form. **Checked by review, not by `words.py`:** a word list cannot tell one it is always the qualified form. **Checked by review, not by the words test:** a word list cannot tell one
sense from another, so the reviewer looks for the bare word in the diff. Where a homonym is settled by sense from another, so the reviewer looks for the bare word in the diff. Where a homonym is settled by
renaming one sense, the old sense moves to a *Not:* line and becomes mechanical. renaming one sense, the old sense moves to a *Not:* line and becomes mechanical.
| Word | Sense | Say | Domain | | Word | Sense | Say | Domain |
|---|---|---|---| |---|---|---|---|
| plan | what the controller would send one node | that node's **declaration** (verb `plan`) | Placement | | plan | what the controller would send one node, until release 6 | that node's **declaration** (verb `plan`) | Placement |
| | what a delivery would do to the mesh | **delivery plan** | Delivery | | | what a delivery would do to the mesh, until release 6 | **delivery plan** | Delivery |
| | the sending of one batch of commits across nodes | **rollout** (verb `plans`) | Delivery | | | the sending of one batch of commits across nodes, until release 6 | **rollout** (verb `plans`) | Delivery |
| | a step a person starts, like the bus's | **planned step** | Core | | | a step a person starts, like the bus's | **planned step** | Core |
| composition | one module's resources for one node at one fingerprint, signed, from release 6 | **composition** | Placement |
| | the function in mesh-sdk that composes them, which the controller and the assigner both run | **mesh-sdk's composition** | Placement |
| | whether the machines a change touches still compose | **composition check** | Delivery |
| membership | the subjects a bus account is issued | **membership** | Provisioning | | membership | the subjects a bus account is issued | **membership** | Provisioning |
| | an account in a group of the machine | **account group** | Placement | | | an account in a group of the machine | **account group** | Placement |
| push | the controller giving nodes their declarations | **send** (verb `push`) | Placement | | push | the controller giving nodes their declarations, until release 6 | **send** (verb `push`) | Placement |
| | a git push | **git push** | The record | | | a git push | **git push** | The record |
| | a phone notification | **push notification** | Operator and conversation | | | a phone notification | **push notification** | Operator and conversation |
| release | a person letting a held delivery go on | **release** | Delivery | | release | a person letting a held delivery go on | **release** | Delivery |
@@ -852,7 +1033,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 | | | 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 | | | 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 | | gate | the judgement on a delivery's first node | **first-node gate** | Delivery |
| | the pull request status | **merge gate** | Delivery | | | the check of what a change makes of the machines it touches, a pull request status until release 4 of ADR 0429 | ~~merge gate~~ (hq); the **composition check** | Delivery |
| | the part that merges a group and moves the source revision | **gate part** (`mesh-delivery-gate`) | 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 | | | 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 | | tier | a level of the mesh | **layer** | Core |
@@ -864,10 +1045,11 @@ renaming one sense, the old sense moves to a *Not:* line and becomes mechanical.
| role | what a seat names, at a scope | **seat**; the bare word in older designs is tracked by issue 872 | Module | | role | what a seat names, at a scope | **seat**; the bare word in older designs is tracked by issue 872 | Module |
| | what an identity may call | **role** | Identity and access | | | what an identity may call | **role** | Identity and access |
| ask | a request for the operator's input | **ask** | Operator and conversation | | ask | a request for the operator's input | **ask** | Operator and conversation |
| | an entry in the build queue | **build request** | Delivery | | | an entry in the build queue | **build request** (an ask on a build stream) | Delivery |
| store | the one database server | **store** | Core | | store | the one database server | **store** | Core |
| | the OCI registry | **artifact store** | Delivery | | | the OCI registry | **artifact store** | Delivery |
| | a node's own copy of its last declaration | **last declaration** | Placement | | | 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 | | 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 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 | | | the upgrade policy that sends nowhere | `record` | Module |
@@ -875,12 +1057,13 @@ 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 | | | 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 | | 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 | — | | | 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, until release 4 of ADR 0429 | **merge check**, **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 | | | a delivery group's verdict | **composed check** | Delivery |
| | the controller's own examination | **self-check** | Health and repair | | | the mesh's own examination | **self-check** | Health and repair |
| | a file in `00-META/checks/` | **hq check** | The record | | | a file in `00-META/checks/` | **hq check** | The record |
| agent | a coding agent | **agent** | Operator and conversation | | agent | a coding agent | **agent** | Operator and conversation |
| | the build seat's holder; from release 4 of ADR 0429 the part `mesh-delivery-builder` | **builder** | Delivery | | | the holder of the seat `node-build-agent` until R4g; from release 4 of ADR 0429 the part `mesh-delivery-builder` | **builder** (the part); **build module** (what builds) | Delivery |
| the user | an account of a wrapped program, or of the bus server | **its account**, **bus account** | Identity and access | | the user | an account of a wrapped program, or of the bus server | **its account**, **bus account** | Identity and access |
| | a human | **operator** or **person** | words every domain uses | | | a human | **operator** or **person** | words every domain uses |
| firewall | what the mesh enforces | **packet filter** | Connectivity | | firewall | what the mesh enforces | **packet filter** | Connectivity |
@@ -893,21 +1076,27 @@ 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* | — | | | 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 | | 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 | | | 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 ## How this page is kept
A new word for an existing thing lands here first, in the domain that owns it, in the same change that A new word for an existing thing lands here first, in the domain that owns it, in the same change that
introduces it in code. A word moves to another domain only with a decision record. A retired word goes introduces it in code. A word moves to another domain only with a decision record. A retired word goes
on a *Not:* line and nowhere else on this page, and `words.py` then fails on it in running prose. A on a *Not:* line and nowhere else on this page, and the words test then fails on it in running prose. A
decision record keeps whatever word it was written with — those are immutable — so a word decision record keeps whatever word it was written with — those are immutable — so a word
retired here may still appear in one, and the *Not:* lines are how to read it. retired here may still appear in one, and the *Not:* lines are how to read it.
**How it is checked** (ADR 0244): **How it is checked** (ADR 0244):
`python3 00-META/checks/words.py`, run by `merge-check.sh` on every pull request, fails on a retired The words test in `00-META/checks/` (`go test ./...`, run by the checker as `mesh/check` on every pull request) fails on a retired
word or a bare identifier in running prose in `00-META/`, `03-DESIGN/`, `AGENTS.md`, `README.md`, and word or a bare identifier in running prose in `00-META/`, `03-DESIGN/`, `AGENTS.md`, `README.md`, and
issues opened from 2026-10-07 — code spans, quotations and link targets excepted — and on a issues opened from 2026-10-07 — code spans, quotations and link targets excepted — and on a
head word that heads two entries or is also retired. The words retired with no scope or with *(tools)* head word that heads two entries or is also retired. The words retired with no scope or with *(tools)*
are copied into the catalogue as `retired-words`, whose own repository check holds the descriptions of are copied into the catalog as `retired-words`, whose own repository check holds the descriptions of
the mesh's tools to them; a change here that retires such a word changes that copy in the same delivery. the mesh's tools to them; a change here that retires such a word changes that copy in the same delivery.
A decision record or research effort proposed to the knowledge base is held to the same words by its A decision record or research effort proposed to the knowledge base is held to the same words by its
rules `retired-word` and `daemon-line`, which read this page (ADR 0290). Homonyms are checked by review. rules `retired-word` and `daemon-line`, which read this page (ADR 0290). Homonyms are checked by review.
+18 -2
View File
@@ -173,6 +173,22 @@ quietly stop being true, and nobody will learn that from a document.
Not test-driven development as a blanket rule — a test written first against undiscovered Not test-driven development as a blanket rule — a test written first against undiscovered
behaviour asserts a guess. The obligation is that every decision has a defender. behaviour asserts a guess. The obligation is that every decision has a defender.
### A bus permission is proven under the real permission
A change that adds or widens what a mesh process reads or writes on the bus (a subscription, a publish, a stream or
key-value read, a consumer) carries an integration test that runs it on a bus user with exactly the production
permissions of that account (mesh-sdk's composition of them), against a real bus server, and that fails with the
bus's permissions violation when the widest new access is not granted. It runs in the forge's check like every
integration test. A reviewer reading code does not see a grant, and a fake bus grants everything (ADR 0560).
How it is checked: a review that approves such a change whose pull request names no such test is the rule skipped.
### A bus permission under the real permission — how it was earned
On 2026-10-27 a change widened what every node agent watches; two reviews read its code and its tests ran against a
fake bus. Live, every node's agent was refused that read by its grant and stopped composing, and the same day the
grants part's user list dropped nine seats' subscriptions (issues 1126 and 1945).
### Tests run at the lowest level that proves them, within a time budget ### Tests run at the lowest level that proves them, within a time budget
Three levels, by how much real system a test uses (ADR 0391): Three levels, by how much real system a test uses (ADR 0391):
@@ -180,7 +196,7 @@ Three levels, by how much real system a test uses (ADR 0391):
| Level | Uses | Budget | Runs | | Level | Uses | Budget | Runs |
|---|---|---|---| |---|---|---|---|
| **Unit** | one piece of logic alone: no database, network, bus, running service or other process | well under a second per test | locally, the tests of what changed, while working; and in the forge's checks | | **Unit** | one piece of logic alone: no database, network, bus, running service or other process | well under a second per test | locally, the tests of what changed, while working; and in the forge's checks |
| **Integration** | the real thing beside it: a database, the bus, a catalogue tree | the repository's whole suite in seconds, never over sixty | only in the forge's checks, once per push | | **Integration** | the real thing beside it: a database, the bus, a catalog tree | the repository's whole suite in seconds, never over sixty | only in the forge's checks, once per push |
| **End to end** | the whole mesh, or a lab mesh, from the outside | — | only when the operator asks | | **End to end** | the whole mesh, or a lab mesh, from the outside | — | only when the operator asks |
- **Test each behaviour at the lowest level that can prove it.** A defect found higher up gets a test at the - **Test each behaviour at the lowest level that can prove it.** A defect found higher up gets a test at the
@@ -193,7 +209,7 @@ Three levels, by how much real system a test uses (ADR 0391):
### Tests within a time budget — how it was earned ### Tests within a time budget — how it was earned
On 2026-10-16 a feature's fixing agents ran the catalogue's whole merge check (about ten minutes), a On 2026-10-16 a feature's fixing agents ran the catalog's whole merge check (about ten minutes), a
throwaway database and a locally built gate during local development. Each round cost tens of minutes and throwaway database and a locally built gate during local development. Each round cost tens of minutes and
caught neither real failure; the forge's checks found both. caught neither real failure; the forge's checks found both.
+8 -4
View File
@@ -48,6 +48,9 @@ the expensive half.
## The playbooks ## 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 | | # | 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 | | [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,17 +60,18 @@ the expensive half.
| [05](05-constitution-sync.md) | Constitution sync | `how-we-build.md` changed a rule the mesh enforces | | [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 | | [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 | | [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 review, one check, 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 | | [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 |
| [10](10-visual-design.md) | Visual design | A page the operator will use is about to be designed or rebuilt: mockups with fixed data, the operator picks, the pick is the reference |
## The cycle is checked ## The cycle is checked
The flow above is a rule, and a rule states how it is checked: The flow above is a rule, and a rule states how it is checked:
[`00-META/checks/cycle.py`](../checks/cycle.py) refuses a to-be design that names no the cycle test in [`00-META/checks/`](../checks/) refuses a to-be design that names no
decision, an `in-progress`/`implemented` design that names no owning code, two issue folders decision, an `in-progress`/`implemented` design that names no owning code, two issue folders
sharing a number or an issue report carrying a status key; a `graduated` research effort that sharing a number or an issue report carrying a status key; a `graduated` research effort that
does not say what it became is refused by the knowledge base's `propose` (ADR 0290). Run it with `records.py`, `index.py` and does not say what it became is refused by the knowledge base's `propose` (ADR 0290). Run it with the records, index and
`words.py` before any HQ merge (`sh merge-check.sh` runs all four). The issue rules the check words tests before any HQ merge: `go test ./...` at the root runs all four, and the checker runs the same as `mesh/check` (ADR 0527). The issue rules the check
once held — `located` with an owner, `resolved` with a fix, a core issue with its replay — are once held — `located` with an owner, `resolved` with a fix, a core issue with its replay — are
refused at the write by the issue tracker's `move` refused at the write by the issue tracker's `move`
(ADR 0275). What the checks cannot see — that code work actually started from a (ADR 0275). What the checks cannot see — that code work actually started from a
+2 -2
View File
@@ -119,7 +119,7 @@ for the operator's Approve on a channel that proves who answered.
- Do not put status in prose. It is the effort's `status`, and the prose must not restate it. - Do not put status in prose. It is the effort's `status`, and the prose must not restate it.
- Do not give a `became` link while the effort is open — playbook 02 gives it at closure. - Do not give a `became` link while the effort is open — playbook 02 gives it at closure.
- Do not write into `03-DESIGN` from an open effort. - Do not write into `03-DESIGN` from an open effort.
- Do not write an effort, or a copy of one, into this repository: `records.py` refuses a new path under - Do not write an effort, or a copy of one, into this repository: the records test refuses a new path under
`01-RESEARCH/`. `01-RESEARCH/`.
## Closing ## Closing
@@ -136,5 +136,5 @@ after every acceptance and raise a condition when one fails. The habits of steps
against the rules, one version per settled set, the cheapest level, nothing left open) are the lead's, read by against the rules, one version per settled set, the cheapest level, nothing left open) are the lead's, read by
the reviewer; of the pre-check, `propose` refuses a link to nothing and a retired word, and the review returns the reviewer; of the pre-check, `propose` refuses a link to nothing and a retired word, and the review returns
a record named but not linked, an untracked deferral and a figure not as measured. That this repository holds a record named but not linked, an untracked deferral and a figure not as measured. That this repository holds
no effort is `records.py`'s `no-records` check, on every pull request. An active effort with no new version no effort is the records test's `no-records` check, on every pull request. An active effort with no new version
for seven days is the smell of research left behind: its lead closes or continues it. for seven days is the smell of research left behind: its lead closes or continues it.
+3 -3
View File
@@ -153,7 +153,7 @@ Implementation state is a third axis, independent of both design and decision.
- Do not edit an as-is document to describe an intention. That is what the to-be layer is for. - Do not edit an as-is document to describe an intention. That is what the to-be layer is for.
- Do not change a decision record's meaning. Supersede it. Correcting a fact it got wrong, while - Do not change a decision record's meaning. Supersede it. Correcting a fact it got wrong, while
the decision stands, is a progressive insight — marked and dated in the record, never silent. the decision stands, is a progressive insight — marked and dated in the record, never silent.
- Do not write a decision record, or a copy of one, into this repository: `records.py` refuses a new path - Do not write a decision record, or a copy of one, into this repository: the records test refuses a new path
under `02-DECISIONS/` other than its README. under `02-DECISIONS/` other than its README.
## How it is checked ## How it is checked
@@ -162,8 +162,8 @@ The rules on a record are the knowledge base's: `propose` refuses one that break
record (its module's table test holds one case per rule), and the four rules that judge the whole record set record (its module's table test holds one case per rule), and the four rules that judge the whole record set
(an accepted decision cited by nothing, a supersession named by one side only, a changed clause nothing (an accepted decision cited by nothing, a supersession named by one side only, a changed clause nothing
changes, an accepted record extending a superseded one) run after every acceptance and raise a condition changes, an accepted record extending a superseded one) run after every acceptance and raise a condition
when one fails. In this repository, `records.py` checks that a design's `decisions:` entries and every when one fails. In this repository, the records test checks that a design's `decisions:` entries and every
"ADR NNNN" in the text are well formed, and that no record is written here; `index.py` checks the reading "ADR NNNN" in the text are well formed, and that no record is written here; the index test checks the reading
order's six topics, which the knowledge base reads. The three habits of graduating step 2 (side by side, clauses order's six topics, which the knowledge base reads. The three habits of graduating step 2 (side by side, clauses
from the direction, the pre-check) are the lead's, read by the reviewer; `propose` refuses a link to nothing, from the direction, the pre-check) are the lead's, read by the reviewer; `propose` refuses a link to nothing,
a deferral no issue tracks and a retired word, and the review returns a record named but not linked. a deferral no issue tracks and a retired word, and the review returns a record named but not linked.
+4 -4
View File
@@ -66,7 +66,7 @@ import read, which every imported issue names (`imported.commit` in `issue-track
``` ```
Then `# NNN. The title`, the title as the tracker holds it, and the symptom as observed, as in the tracker's Then `# NNN. The title`, the title as the tracker holds it, and the symptom as observed, as in the tracker's
report. **No status key**: `cycle.py` refuses `status:`, `located-in:`, `fixed-by:`, `amended-design:`, report. **No status key**: the cycle test refuses `status:`, `located-in:`, `fixed-by:`, `amended-design:`,
`replay:` or `replay-none:` in any report, because the tracker holds them and two places holding one fact `replay:` or `replay-none:` in any report, because the tracker holds them and two places holding one fact
drift. drift.
@@ -91,7 +91,7 @@ import read, which every imported issue names (`imported.commit` in `issue-track
ADR 0281). ADR 0281).
4. Resolve. `issue-tracker.move` to `resolved`, which the tracker takes only from `fixing`, with `fixed-by`, and, if the root cause was a design gap, 4. Resolve. `issue-tracker.move` to `resolved`, which the tracker takes only from `fixing`, with `fixed-by`, and, if the root cause was a design gap,
run playbook [02](02-graduation.md) and give `amended-design`. **A core issue** — its `located-in` run playbook [02](02-graduation.md) and give `amended-design`. **A core issue** — its `located-in`
names the controller, the node-engine, the tool runner, the SDK, or the catalogue's bus, forge or build names the controller, the node-engine, the tool runner, the SDK, or the catalog's bus, forge or build
agent — resolves with `replay`, the id of its replay in mesh-lab's replays register, proved to fail on agent — resolves with `replay`, the id of its replay in mesh-lab's replays register, proved to fail on
the commit before the fix and pass on it, or with `replay-none` saying why none is possible the commit before the fix and pass on it, or with `replay-none` saying why none is possible
(ADR 0237); (ADR 0237);
@@ -107,7 +107,7 @@ import read, which every imported issue names (`imported.commit` in `issue-track
it, pointing at the new issue. Somebody arriving at a record to learn why the code is the way it it, pointing at the new issue. Somebody arriving at a record to learn why the code is the way it
is must not have to already know there was a sequel. is must not have to already know there was a sequel.
- A number never moves. A folder carries the number the tracker gave; a folder whose number collides with - A number never moves. A folder carries the number the tracker gave; a folder whose number collides with
another (one numbered before the tracker, or by hand) is a fault, and `cycle.py` refuses two records another (one numbered before the tracker, or by hand) is a fault, and the cycle test refuses two records
sharing a number. sharing a number.
- An issue whose answer is a general lesson should also have that lesson written where a search by - An issue whose answer is a general lesson should also have that lesson written where a search by
the symptom's literal words finds it, so the next person searching it finds it. Both, not either. the symptom's literal words finds it, so the next person searching it finds it. Both, not either.
@@ -117,6 +117,6 @@ import read, which every imported issue names (`imported.commit` in `issue-track
- **A rule states how it is checked.** The cycle's rules on an issue — a status one of the six, `diagnosing` and - **A rule states how it is checked.** The cycle's rules on an issue — a status one of the six, `diagnosing` and
`fixing` with a worker and no other current one, `located` with an owner, `resolved` only from `fixing` and with a `fixing` with a worker and no other current one, `located` with an owner, `resolved` only from `fixing` and with a
fix, a core issue with its replay, `wontfix` with a reason — are refused at the write by fix, a core issue with its replay, `wontfix` with a reason — are refused at the write by
`issue-tracker.move` (mesh-catalog `modules/mesh-issues`, its table test over the rules). `cycle.py` checks what `issue-tracker.move` (mesh-catalog `modules/mesh-issues`, its table test over the rules). the cycle test checks what
this repository still holds: no two folders share a number, every report has frontmatter with the day it was this repository still holds: no two folders share a number, every report has frontmatter with the day it was
`opened:`, and no report carries a status key. `opened:`, and no report carries a status key.
+16 -14
View File
@@ -9,19 +9,19 @@ an hour, and said to the operator if it runs past.
A design becomes **milestones**, and each milestone becomes **issues**: A design becomes **milestones**, and each milestone becomes **issues**:
- **A milestone is usable on its own.** Once its delivery plan is done, the operator or the mesh can do - **A milestone is usable on its own.** Once it is delivered, the operator or the mesh can do
something new, said in one sentence ("a Claude Code session is mirrored into a thread on the bus"). something new, said in one sentence ("a Claude Code session is mirrored into a thread on the bus").
A milestone that changes nothing anyone can use is not a milestone; fold it into the next. A milestone that changes nothing anyone can use is not a milestone; fold it into the next.
- **A milestone is one release** of playbook [07](07-feature-branches.md): prepared, developed whole, reviewed, - **A milestone is one release** of playbook [07](07-feature-branches.md): prepared, developed whole, reviewed,
checked and delivered as one, never cut for its size or for time (the operator, 2026-10-17). checked and delivered as one, never cut for its size or for time (the operator, 2026-10-17).
- **Milestones are cut by release order.** A milestone ends where one repository's change must be released before - **A milestone is one piece of functionality, never cut by an order between its parts** (the operator, 2026-10-21:
another can use it (a new manifest word before the catalogue reads it, an SDK change before its users, until a milestone may span several modules, repositories and dependencies such as an SDK change; 2026-10-22: "a milestone
design 47's milestones 3 and 6 lift those orders); the lead checks this when cutting, so no milestone needs a is a milestone, we shouldn't cut it"). Which of its modules reaches the machines first (a reader of a new manifest
second release. The orders to look for are those playbook [07](07-feature-branches.md) step 0 item 6 names (an SDK word before the modules that use it, a provider before its consumers) is the assigner's rules' (design 47); an SDK
change before its users, a new wire key before the node that reads it, the first module of a repository the catalogue change is built into its users. The parts such an order touches are found in playbook [07](07-feature-branches.md)
never built, a part that needs a decision first); a milestone cut by such an order may be usable only by the next one, step 0 item 6 and become issues of the same milestone; an order those rules cannot keep is an issue of the build
and says so in its outcome. A milestone the prepare step finds holding such an order is split into milestones in the and delivery system, not a cut.
design, each one release, never carried as parts of one (2026-10-18: design 47's release 1-2 became six). Until 2026-10-22 this rule cut milestones by release order (design 47's release 1-2 became six milestones).
- **An issue is one repository's part of a milestone,** sized so a fixing agent finishes it inside the - **An issue is one repository's part of a milestone,** sized so a fixing agent finishes it inside the
milestone's development time. Issues that must land in order are linked `depends-on`. milestone's development time. Issues that must land in order are linked `depends-on`.
@@ -38,8 +38,9 @@ A design becomes **milestones**, and each milestone becomes **issues**:
4. **Break it into milestones.** Write them into the design, in a section *Milestones*: for each, its number, 4. **Break it into milestones.** Write them into the design, in a section *Milestones*: for each, its number,
its one-sentence outcome, the acceptance tests of the design it proves, the repositories it touches, and its one-sentence outcome, the acceptance tests of the design it proves, the repositories it touches, and
what it depends on. Order them so each is usable as soon as it is delivered, the smallest useful one what it depends on. Order them so each is usable as soon as it is delivered, the smallest useful one
first. A milestone is named by what it lets someone do; how it ships is shaped by the plan its changes would first. A milestone is named by what it lets someone do; how it ships is shaped by the modules its changes move
have, asked through `mesh-delivery.what-if`: the modules it moves and their fan-out (ADR 0401 §6). and the nodes that run them (ADR 0401 §6), read from the indexer's fingerprints through the status view's
`indexer_modules` and `indexer_candidates`, and from the nodes' haves (`node_haves`).
5. **Ask the operator to confirm the milestones**, in a short table: number, outcome, the acceptance tests it 5. **Ask the operator to confirm the milestones**, in a short table: number, outcome, the acceptance tests it
proves, repositories, the parts each repository builds. Change what they change. proves, repositories, the parts each repository builds. Change what they change.
6. **Open the issues.** For each confirmed milestone, `issue-tracker.open` one issue per repository part, its 6. **Open the issues.** For each confirmed milestone, `issue-tracker.open` one issue per repository part, its
@@ -49,7 +50,7 @@ A design becomes **milestones**, and each milestone becomes **issues**:
7. **Flip the status.** `status: in-progress`, `updated:` today; the design change goes through playbook 7. **Flip the status.** `status: in-progress`, `updated:` today; the design change goes through playbook
[08](08-small-fixes.md) (one hq pull request). [08](08-small-fixes.md) (one hq pull request).
8. **Hand each milestone to playbook 07, one at a time.** The milestone is one release of playbook 07; its issues 8. **Hand each milestone to playbook 07, one at a time.** The milestone is one release of playbook 07; its issues
are the release's issues; the next milestone starts when the previous one's delivery plan is done. A design waits are the release's issues; the next milestone starts when the previous one is delivered. A design waits
while another design's milestones are being built: one design is built at a time (playbook while another design's milestones are being built: one design is built at a time (playbook
[07](07-feature-branches.md)), so this step starts only when the design before it is delivered. [07](07-feature-branches.md)), so this step starts only when the design before it is delivered.
9. **On completion of the last milestone**, run the "when something ships" section of playbook 9. **On completion of the last milestone**, run the "when something ships" section of playbook
@@ -67,11 +68,12 @@ A design becomes **milestones**, and each milestone becomes **issues**:
## How it is checked ## How it is checked
Read by the lead and the reviewer today; a check in hq's merge-check is tracked by issue 845. Read by the lead and the reviewer today; a check among hq's Go tests is tracked by issue 845.
- A design in `status: in-progress` has a *Milestones* section, each milestone with its outcome and its - A design in `status: in-progress` has a *Milestones* section, each milestone with its outcome and its
issues; an in-progress design without one is the smell of building without a breakdown. issues; an in-progress design without one is the smell of building without a breakdown.
- Every issue a milestone names exists in the tracker (`issue-tracker.get`), and an issue in `fixing` names - Every issue a milestone names exists in the tracker (`issue-tracker.get`), and an issue in `fixing` names
its milestone in its report. its milestone in its report.
- A milestone whose issues are all `resolved` has had its delivery plan done; its outcome sentence and - A milestone whose issues are all `resolved` is delivered (the status view said `delivered` for its source
revision); its outcome sentence and
acceptance tests are proven on the mesh before the next milestone starts. acceptance tests are proven on the mesh before the next milestone starts.
+20
View File
@@ -105,3 +105,23 @@ time and the scaffolding was not.**
- **Never rotate during an adoption.** Rotation is a separate act, afterwards, deliberately. - **Never rotate during an adoption.** Rotation is a separate act, afterwards, deliberately.
- **A data directory is never removed by the mesh** (ADR 0030), - **A data directory is never removed by the mesh** (ADR 0030),
and that protects against the mesh only — not against a disk or a mistaken command. and that protects against the mesh only — not against a disk or a mistaken command.
## Moving a module's directory
When a fix moves what a container mounts to a new path (issue 1843: only-office's working directories
moved into a root-only box), the old directory does not go away by itself, and it is closed in three steps.
1. **The move.** The new directory gets a new id at its new path. The old one keeps **its old id at its old
path** for as long as it holds data, with the mode it should now have: the node-engine leaves a directory
it finds at a path under a new id as it found it, and sets no mode when the owner differs (issue 339), so
only the old id closes it. The node-engine removes a directory that is no longer declared only when it is
empty; a non-empty one is kept on disk with no further cleanup.
2. **The deletion, on the operator's word.** Its contents are data, so they are removed only after the
operator's yes, through `<node>/node-files.delete`. That tool moves into the node's trash: the disk space
comes back only when the trash is emptied (issue 1870: no tool does that yet).
3. **The declaration leaves** in a second small fix, once the directory is empty (issue 1854).
Plan the three in the first fix's issue, so the second fix is not found later by accident.
A service that starts on empty working directories may need minutes before it answers (OnlyOffice: about
two): the live check after the delivery waits for it before calling the move broken.
+386 -114
View File
@@ -19,48 +19,82 @@ A small fix (one repository, one issue, about an hour of work) follows
minutes took forty-one. minutes took forty-one.
- **Finished work that never ships.** A feature that kept taking new issues held thirteen landed issues - **Finished work that never ships.** A feature that kept taking new issues held thirteen landed issues
undelivered for a day: an issue done at 21:09 was still not on the machines at 15:00 the next day. undelivered for a day: an issue done at 21:09 was still not on the machines at 15:00 the next day.
- **Work that waits and nobody notices.** Most lost time was not work at all, but a check, a delivery plan - **Work that waits and nobody notices.** Most lost time was not work at all, but a check, a delivery
or an agent waiting on something that had stopped, unseen. or an agent waiting on something that had stopped, unseen.
- **Review that keeps hardening.** One fix went through seven review rounds in five and a half hours, each - **Review that keeps hardening.** One fix went through seven review rounds in five and a half hours, each
asking for more hardening than its issue asked (issue 704, 2026-10-14). A round is for what makes the asking for more hardening than its issue asked (issue 704, 2026-10-14). A round is for what makes the
change wrong; everything else is an issue of its own. change wrong; everything else is an issue of its own.
- **The first check after the whole branch is written.** A feature's catalogue branch, pushed once after - **The first check after the whole branch is written.** A feature's catalog branch, pushed once after
hours of local work, met its first check on 2026-10-16 and failed on 29 problems, TypeScript errors among hours of local work, met its first check on 2026-10-16 and failed on 29 problems, TypeScript errors among
them, that a unit suite run continuously while working finds at once. them, that a unit suite run continuously while working finds at once.
- **A green pull request nobody marks.** An approved, green pull request that was not marked ready waited - **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: 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. 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 check asked again and again.** Build capacity is the mesh's scarcest resource ("we don't have more
processing power", the operator, 2026-10-26), and a release that ran a try before its review, a try after it
and a check per fix round on an open pull request spent it several times over: every push to an open pull
request asks a new check, of every member for a group. A release runs one check ("every release should only
run 1 check or 1 try", the operator, 2026-10-26; ADR 0548).
- **A node agent's bus reads, proven only by review.** On 2026-10-27 a change widened what every node agent watches
on the bus; two reviews read the code, no test ran it under a node's real permissions, and once it was live every
node's agent was refused that read and stopped composing (issue 1126). How-we-build's rule "A bus permission is
proven under the real permission" (ADR 0560) is what the review now asks for.
- **A module proven only after it reached the machines.** The mail modules were delivered with unit tests - **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. only, then failed live three times (issues 859, 865, 868) and took five more hours of fixes.
## The shape ## The shape
One feature is **one branch name**, **one worktree per repository**, **one pull request per repository**, One feature is **one branch name**, **one worktree per repository**, **one pull request per repository**,
opened **once**, after the review. It runs in five steps, and its time is bounded. A milestone has no phases opened **once**, after the review, and **one check** (ADR 0548). It runs in five steps, and its time is bounded. A milestone has no phases
(the operator, 2026-10-17): these are its steps, and a report names the step it is in: (the operator, 2026-10-17): these are its steps, and a report names the step it is in:
| Step | What happens | Bound | | Step | What happens | Bound |
|---|---|---| |---|---|---|
| 0. Prepare | the lead settles the open questions and writes one context file per repository | 15 minutes | | 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 | | 1. Develop | one fixing agent per repository builds every issue locally, the whole unit suite before every commit, the integration tests compiled (those that exercise the change are run by name before the push, step 3); nothing is pushed | 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 | | 2. Review | one reviewer reads all of it, the local commits, judging the integration tests by reading them against the code; 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 | | 3. Check | push the reviewed heads once and open the pull requests together; the checker's one check runs the integration tests; no further review unless the check fails | one check run |
| 4. Deliver | merge; the delivery plan builds, publishes and sends the whole feature | its delivery plan | | 4. Deliver | merge; the source revision moves, each module is built once and wanted on its nodes, its canary first | until the view says the source revision `delivered` |
**One milestone is one release** (the operator, 2026-10-17). A milestone is developed whole, reviewed once, **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 pushed once, checked 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 merge check, an Approve and a delivery of waiting, so the fewest cuts is the fastest. Nor is it cut for an
release order when the design is broken up (playbook [04](04-build-handoff.md)); a release-order dependency found order between its parts (the operator, 2026-10-22: "a milestone is a milestone, we shouldn't cut it"): which module
inside a milestone is a fault of that cut, said to the operator, never a silent extra step. reaches the machines before which is the assigner's rules' (a build and a composition before the want, a provider
before its consumers, the canary before the rest; design 47), and that is what they are for. An order they cannot
keep is a fault of the build and delivery system, opened as an issue there, never a cut and never a step a session
takes by hand.
A milestone is one piece of functionality, possibly spanning several modules, repositories and dependencies such as
an SDK change (the operator, 2026-10-21). Its pull requests in every repository share the milestone's branch name,
so they are one delivery group (ADR 0482, which holds that a change across repositories is one group, one plan and
one Approve, and that its developer orders no release). Nothing of the milestone merges until every issue of it is
built, reviewed and checked; then the one Approve releases it all as one source revision, the assigner's rules
keep a provider before its consumers, and an SDK change is built into the modules that use it. A small fix found during the milestone rides in this
group, as playbook [08](08-small-fixes.md) says.
**One design is built at a time** (the operator, 2026-10-17). While the milestones of one design are being **One design is built at a time** (the operator, 2026-10-17). While the milestones of one design are being
built, no milestone of another design starts: its issues stay `open`, and nobody moves them to `diagnosing` or built, no milestone of another design starts: its issues stay `open`, and nobody moves them to `diagnosing` or
`fixing`. The next design starts when the last milestone of the one being built has its delivery plan done. A `fixing`. The next design starts when the last milestone of the one being built is delivered. A
lead who finds another design's milestone already in `fixing` asks its owner before starting its own. A small lead who finds another design's milestone already in `fixing` asks its owner before starting its own. A small
fix ([playbook 08](08-small-fixes.md)) and an issue that is in no design's milestones are not held by this rule. fix ([playbook 08](08-small-fixes.md)) and an issue that is in no design's milestones are not held by this rule.
**Scope is fixed at the prepare step.** An issue found during a feature goes to the next milestone or the next feature, **Scope is fixed at the prepare step.** An issue found during a feature never goes into the running milestone, unless it
never into the running one, unless it blocks the running one. blocks the running one: it is fixed in between or right after the delivery, as the next paragraph says, or goes to the
next milestone or the next feature.
**An issue opened along the way is fixed by the session that opened it** (the operator, 2026-10-21): it is never only
listed under "Opened along the way" and left. A small one (one repository, about an hour) is fixed in between, as a
small fix ([playbook 08](08-small-fixes.md)) while the milestone waits on a check, an Approve or a delivery; the
rest are fixed right after the milestone's delivery, before the next milestone starts, together on one branch where
they share a repository. Either way it stays out of the running milestone's branch and pull request, unless it blocks
the running milestone, so the scope above holds. Only an issue in another design's milestones (one design is built at
a time), or one that needs a decision or the operator first, waits, and the report says which and why. Each detailed report's line "Opened along the way" says for every
issue whether it is being fixed in between, after the delivery, or waits, and on what.
## Step 0 — Prepare (the lead, larger model) ## Step 0 — Prepare (the lead, larger model)
@@ -75,7 +109,10 @@ never into the running one, unless it blocks the running one.
clone stays on `main`** (the operator, 2026-10-17): no work is done in it and no branch is checked out clone stays on `main`** (the operator, 2026-10-17): no work is done in it and no branch is checked out
there, so a session that opens in it reads what holds now; fetch and fast-forward it before reading. there, so a session that opens in it reads what holds now; fetch and fast-forward it before reading.
3. **Settle every open question** in the feature's issues before any agent starts: on the issue, through 3. **Settle every open question** in the feature's issues before any agent starts: on the issue, through
the knowledge base, or by asking the operator. A fixing agent that meets an undecided design question the knowledge base, or by asking the operator. **Read what is decided before designing** (`knowledge-base.ask`):
a question about delivery, checks or builds is most likely answered in design 47 and its decisions already (the operator,
2026-10-26: "that is simply our delivery plan and milestones, we've already researched it"); propose new research
only for what no record decides. A fixing agent that meets an undecided design question
stops, and its run is spent again. stops, and its run is spent again.
4. **Write one context file per repository**, `.work/<slug>/<repo>.context.md`. It is what a fixing agent 4. **Write one context file per repository**, `.work/<slug>/<repo>.context.md`. It is what a fixing agent
reads instead of rediscovering the code, and it is reused by every agent and every round: reads instead of rediscovering the code, and it is reused by every agent and every round:
@@ -87,14 +124,18 @@ never into the running one, unless it blocks the running one.
- what not to touch, and what lands beside it in other repositories; - what not to touch, and what lands beside it in other repositories;
- the local facts: the worktree, the branch, the check commands, a temporary directory for tests outside - the local facts: the worktree, the branch, the check commands, a temporary directory for tests outside
the home (`TMPDIR`), and the unit tests to run (never a database server: tests that need one are the home (`TMPDIR`), and the unit tests to run (never a database server: tests that need one are
integration tests, which `mesh-controller.try` and the forge's checks run). integration tests, which the pull request's one check runs).
When the reading is large, a read-only agent on the smaller model answers the lead's exact questions with When the reading is large, a read-only agent on the smaller model answers the lead's exact questions with
file:line, and the lead writes the file from its answers. file:line, and the lead writes the file from its answers.
5. **Write the findings into the issues** (the operator, 2026-10-17: the better the content of the issues, the 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 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 `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
modules its delivery plan moves, to which machines); and for a group, its merge order. The context file 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 merge moves, read from the indexer's candidates of its group, and 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. 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. When the reading is large, it starts with a read-only survey agent on the smaller model.
**Each issue carries its acceptance criteria, written by the lead** (the operator, 2026-10-18: the larger **Each issue carries its acceptance criteria, written by the lead** (the operator, 2026-10-18: the larger
@@ -111,19 +152,38 @@ never into the running one, unless it blocks the running one.
2026-10-17), into issues of about 30 minutes each, one change each, in the order they land: the bound of a 2026-10-17), into issues of about 30 minutes each, one change each, in the order they land: the bound of a
fixing agent's issue is then a real bound. On 2026-10-17 one issue held four changes sized at four to six fixing agent's issue is then a real bound. On 2026-10-17 one issue held four changes sized at four to six
hours; it ran past its 30-minute bound for over an hour, and the reports could only say "still on part D". hours; it ran past its 30-minute bound for over an hour, and the reports could only say "still on part D".
6. **Look for the orders a release hides, before any agent starts** (2026-10-18: design 47's release 1-2 was cut 6. **List every part the change reaches, before any agent starts**, so each is an issue of the milestone and
as one milestone and its prepare step found three orders inside it, each a cut of its own). Read, for each nothing is found on the way. Read, for each repository of the milestone, what it takes from the others and how:
repository of the release, what it takes from the others and how: an SDK change still ships before its users the SDK's users (they pin an SDK commit, and a pin to a commit no branch holds is a download that fails later,
(they vendor a pinned SDK, and a pin to a commit no branch holds is a download that fails later, issue 1148); issue 1148); every program that parses a manifest strictly (search `ParseManifest` across every repository, with
a new key on the wire between the controller and the node-engine is sent only to a node that says it reads it, the SDK commit each pins), which must take an SDK that knows a new manifest word (2026-10-21: design 60's
since the node-engine's decoder refuses an unknown key; the first module of a repository the catalogue has never catalog milestone found mesh-build-go and the delivery's indexer reading manifests with an SDK from before the
built registers only by the route the controller allows for it; and a part that needs something the bus or a word); the SDK's list of a defined seat's verbs, which must name a verb a module adds (2026-10-21: node-files' two
manifest cannot yet give (an event only a system account hears, a bucket whose keys expire) needs its decision new verbs were refused on the claim); the node-engine's decoder for a new key on the wire; the route by which a
first. Each order found is said to the operator with the cut it asks for, never built around. repository's first module registers; and a part that needs something the bus or a manifest cannot yet give, which
needs its decision first. Each such part is an issue of the milestone and its pull request a member of the group;
the order in which they reach the machines is the assigner's rules' (the operator, 2026-10-22), never a cut.
Where those rules cannot keep an order (a node told of a key before it reads it, a module using a word before its
readers are delivered), that is an issue of the build and delivery system, opened with `issue-tracker.open` and
said to the operator.
7. **Write fixtures from the shape the other side writes, never from memory** (2026-10-18: a watchman's tests 7. **Write fixtures from the shape the other side writes, never from memory** (2026-10-18: a watchman's tests
mirrored a build key the controller never writes, and every test passed): copy a value from the writer's code or mirrored a build key the controller never writes, and every test passed): copy a value from the writer's code or
from the live bus, and name where it came from. from the live bus, and name where it came from.
8. **Move the issues to `fixing`**, `by` naming the feature's agent for that repository. 8. **Answer three questions in the milestone file** (`.work/<slug>/milestone.md`; research 071, issue 1941), written by the
lead from this template before any agent starts. Each answer is a fact found, not a hope:
```
Lifetime of what the work creates (research 071, a):
- <bucket key or stream> | written by <module> | deleted by <module> on <event>, or time to live <duration>
(one line for every key-value bucket key and stream the work creates or starts writing; "none created" if none)
Trying the switch-over on one node first (b):
- <which node goes first, and what is read there before the rest>
- or: the operator chose no first trial on one node, on <date>; what that risks: <one sentence>
Integration level (c):
- <the one check that runs it: the group's pull requests, one check each, none before the review>
- what the check does not cover: <the parts of the group that meet only live>
```
A key or stream with no one who deletes it and no time to live is a leak and is settled before work starts.
9. **Move the issues to `fixing`**, `by` naming the feature's agent for that repository.
## Step 1 — Develop (one fixing agent per repository, smaller model) ## Step 1 — Develop (one fixing agent per repository, smaller model)
@@ -133,27 +193,39 @@ never into the running one, unless it blocks the running one.
the next instruction, so its context is paid for once. 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 - **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 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 change breaks tests in packages it never touched. Each issue's first test fails before its change, on an assertion about what the
after; `gofmt`, `go vet` and the whole unit suite stay green. **The tests are written from the issue's 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 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 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 that compiles and is run by the pull request's one check. The integration tests (those that start a store, a bus
process) wait for the check step, where `mesh-controller.try` runs them first. or a process) are compiled with `go vet -tags integration ./...` while developing and run by that check; before the group's push only the tests that exercise the change are run by
- **Nothing is pushed** and no pull request is opened in this step. Never land on a red check: run the name (step 3), nothing else.
checks, read the result, then land in a separate step. - **Nothing is pushed and no pull request is opened in this step, and there is no try** (ADR 0548; the operator,
2026-10-26: "every release should only run 1 check or 1 try"). When every issue is committed, the lead brings each
feature branch up to the current trunk (fetch by name, then merge) and the review reads the local commits. Never call
`mesh-controller.try`, `mesh-delivery-checker.try` or `mesh-delivery-checker.checker_try`; one is run only when the operator asks for it. A reviewer
or a session that wants the code on the forge may push a branch that has no pull request: it runs nothing. Never land
on a red check: 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 - **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; message ("issue 805: landing in mesh-catalog"); reach the mesh only through the mesh MCP server, never ssh;
never push, open, merge or close a pull request; never write to a store's database by hand, a schema change never push, open, merge or close a pull request; never call a try; never write to a store's database by hand, a schema change
is a numbered migration and the consolidated schema; Go only unless the context file says otherwise; never 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`, move another issue; **leave the integration tests to the check** (no store or bus started by hand): compile them
no store or bus started by hand): compile them with `go vet -tags integration ./...` and leave the running to with `go vet -tags integration ./...`. A single integration test may be run locally against an embedded bus when
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 it is the fastest way to find a cause, said in the review report and the pull request; before the group's push 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 tests that exercise the change are run this way, by name (step 3). After a failed check it
check results, anything you stopped on. usually pays to run that repository's whole integration level locally (`go test -tags integration -count=1 ./...` in each
module), by the lead or by the fixing agent on the lead's word, before the one more push: a verdict names only the first failed
test (issue 1921), and on 2026-10-23 R4g's faults surfaced one per run until a local run listed them all in a minute (a
guideline, the operator). Report back in under 150 words: commits, tests added and that they failed first, the
local results, anything you stopped on.
- **An agent that finds the code differs from its context file stops and reports** (file:line); the lead - **An agent that finds the code differs from its context file stops and reports** (file:line); the lead
settles it, updates the context file, and continues the agent. settles it, updates the context file, and continues the agent.
- **Two repositories that depend on each other** keep their order in the context files: the reader before the - **Two repositories that depend on each other** keep their order in the context files: the reader before the
writer, the controller's new manifest word before the catalogue manifest that uses it. writer, the controller's new manifest word before the catalog manifest that uses it.
- **Several agents in one repository** (only for a large repository with independent areas): each works on a - **Several agents in one repository** (only for a large repository with independent areas): each works on a
local branch of its own from the feature branch's head and lands by fast-forward under a lock local branch of its own from the feature branch's head and lands by fast-forward under a lock
(`flock <feature worktree>.lock git -C <feature worktree> merge --ff-only <its branch>`), after its unit (`flock <feature worktree>.lock git -C <feature worktree> merge --ff-only <its branch>`), after its unit
@@ -162,8 +234,14 @@ 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) ## 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 1. **The reviewed tree is the tree that is pushed**: every feature branch was brought up to the current trunk
the tree that is pushed. before the review (step 1). The check runs 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. Every verdict names the
trunk it merged onto ("pass <head> on <trunk>"), and the lead reads that trunk against the repository's main
at once: a trunk behind main means the source revision (source.main) lags, and every check in that
repository is judged on it until it moves, so the operator's Approve will merge nothing. Say it then, not at the
merge (issue 1726: on 2026-10-24 the gate dropped two merges it heard before it held its seat, and a
reviewed, approved pull request in each of the two repositories was refused at its Approve).
2. **One reviewer reads every repository's local commits** together, against the context files and the 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 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 its id for every later round, for the extra cycle after a failed check (step 3) and for its review record, so it
@@ -172,81 +250,259 @@ never into the running one, unless it blocks the running one.
It says which round it is in ("round 1 of at most 3"). 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 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. 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.** Fixing agents only compile it, and no run precedes the review (step 1), so the reviewer judges it by reading; the check that follows 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. A review that finds a fault only by reading is the cheaper run. 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 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. 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 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 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 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. 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).
**A test fixture writes state in the form its real writer writes it** (for example a resolved config value with its
source, as the controller stores it); a fixture that agrees with the mistake proves the mistake. How it is checked:
the review asks it of every new fixture, and says which writer it compared with.
6a. **The reviewer reads the milestone file's three answers** (step 0, item 8) against the change: it lists every bucket
key and stream the diff creates or writes (search the diff for the bucket and stream names) and finds each in the
lifetime answer with a deleter and event, or a time to live; it reads the switch-over answer and checks it was done as
written or that the operator's choice is recorded; it reads the integration-level answer against the check the pull requests will ask. A missing answer, or a key or stream the diff creates that the answer omits, is a change asked.
7. **A finding outside the issues' done-when and their acceptance criteria is never a change asked, in any round.** More hardening, a 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 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 report, and the round does not wait for it. What a round asks is only what makes the change wrong against
its issues, their acceptance criteria and its decisions. its issues, their acceptance criteria and its decisions.
8. **A timer that writes state, or a rule fixed in code that overrides what a person sets, makes the change
wrong.** The mesh is NATS-native: a part says what happened as an event and acts on events; nothing polls
and rewrites state on an interval, and a policy the operator can change is a setting the module declares,
never a constant. The reviewer reads every new or changed interval, ticker, sleep loop and periodic
run-once in the change and asks of each: what event should drive it instead, and what setting turns it off.
One with no answer is a change asked. On 2026-10-23 the forge module's loop rewrote every repository's
branch protection every ten minutes from rules in its code and undid the operator's changes at the forge
(issue 1903).
9. **At the second instance of a fault kind in a review, the developer searches for every instance and names each in the
report.** On 2026-10-25 a "busy, return at once" guard that dropped a newer request was found in three places,
one review after another. The developer writes the kind in one line, searches the repository (`git grep`) for
its shape, fixes every hit in one change and lists each file and line in the report. How it is checked: a
report to the second round naming a kind and one place only, followed by a third round on the same kind, is
this rule skipped.
10. **The reviewer asks two questions of every change and every design, and answers both in its report** (the
operator, 2026-10-28: "design everything NATS-native"; "repositories are fully mesh agnostic"). The first:
*where does each piece of state live, and how does each seam (an integration, data, a part talking to
another) travel?* Anything that is not bus state, a stream or a seat's verb on the bus is a finding, even when
an accepted record put it there. The second: *does a repository hold anything about this mesh's setup, such as
node names, node-specific placement or settings?* That is a finding: repositories are mesh-agnostic. On
2026-10-28 a placement design kept placement in repository files with node names, because the accepted record
it followed said so and nobody checked it against these two principles. A finding from either question makes
the change wrong, whatever record it follows; the reviewer names the record so it can be superseded. How it is
checked: the review report has a line for each question with its answer, and the lead reads both before the
push; a report without them is not an approval.
## Step 3 — Check (no review) ## Step 3 — Check (no review)
1. **Push every feature branch, try it, then open one pull request per repository, together.** 1. **Push the reviewed heads once, then open one pull request per repository, together** (ADR 0548; the operator,
**The integration tests run once before the pull requests are opened**: `mesh-controller.try` with the 2026-10-26: "every release should only run 1 check or 1 try"). **No pull request, draft or not, exists before the
repository and the pushed branch runs the repository's `merge-check.sh` (main's version) on the merge, in the review approves the heads**, and no fix round is pushed to an open pull request: every push to an open pull request
toolchain containers and against the throwaway store and bus a merge check gets, and answers with an id to asks a new check, of every member for a group, and the mesh's build capacity is spent on it ("we don't have more
read back. A try that fails goes to step 3 item 3 as a failed check does; the pull requests are opened only processing power"). The integration tests run once, in the checker's merge check on the merge with the trunk's head, at the unit
when every try passed (the operator, 2026-10-18: three pushes in a row failed their first merge check on and the integration level, through the build module of the repository's language and by that language's convention
faults a try finds in minutes). Never `merge-check.sh`, `go test -tags integration` or a store or bus started (ADR 0445, ADR 0529); the check waits on the language's check stream until a build node has a free slot
by hand on the machine (the operator, 2026-10-18). Pull requests in two or (ADR 0525 §1). **No try:** never call `mesh-controller.try`, `mesh-delivery-checker.try` or `mesh-delivery-checker.checker_try`, except when the
operator asks for one. While developing, no `go test -tags integration` run of a whole module or level, and no store or bus started
by hand on the machine (the operator, 2026-10-18).
**Before a group's single push, the integration tests that exercise the change are run locally against an embedded
bus**, picked by name (`go test -tags integration -count=1 -run '<names>'`): the tests in the touched packages that
start the part whole, not a whole module's or level's integration run, and not only a compile with `go vet`; a
change in how a node composes or applies always has such a test (2026-10-28: a compiled-only group failed in the one
check on what a local run shows). How it is checked: the pull request's description names those tests and their
results.
**While a group is being checked, nothing else is merged or pushed into a repository that is one of its members:**
a moved main asks the group's check again. How it is checked: the lead's wait list names the group's members, and
`mesh-delivery.show` shows one check per head.
**A group is one check.** Before the push, every member pins the group's own mesh-sdk head exactly (the checker refuses an
older commit of the same branch), then all members are pushed and opened together, so the group's check runs once. A head the review replaces
counts for nothing against a running check; say so, and ask no other until the new head is reviewed.
**Before a session trusts an agent's "pushed", it reads the forge branch's head** (`git ls-remote origin refs/heads/<branch>`) and compares it with the commit reviewed; an agent may commit on a detached HEAD and report a push that never happened (2026-10-26: the first check of a reviewed fix failed "not our ref"). The pull request is opened only on that read head. Pull requests in two or
more repositories that share the branch name are one delivery group (ADR 0239 §2). more repositories that share the branch name are one delivery group (ADR 0239 §2).
**A temporary mode that merges by hand** (the lead merges because the normal Approve and merge path is not
running): the pull request's one check still runs the integration level, and the lead names its verdict in the
pull request's description. A try with tests `.` is asked only when the operator asks for one, and then named there with its id and verdict (research 071).
Where a session pushes through the forge tool `gitea_commit` (one commit on the forge's current base), the 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: 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 `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. 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. It is never a gate. **Beside it, the description names the session that pushed the head**, one line
2. **The forge's merge checks run the integration tests again, as the gate** (`mesh/merge-gate`, `mesh/repo-check`). No `Pushed-by: <who@node>` (ADR 0505 §5): when the head cannot merge because its check failed after the Approve (a
conflict, a failed check, a rejected group), the cure is that session's new head, and mesh-delivery tells the
operator only after two hours with no new head, naming the branch's author and this session. The session that
pushed watches its own pull request (the instruction follow-what-you-start) and pushes the new head well inside
those two hours. A description without the line names the branch alone.
**The description names its issues as `issue <n>`, and only its own** (2026-10-21: okioki #9 listed its four
issues bare, `Issues: 1608, 1646, …`, and named a milestone-2 issue as `issue 1654` and a follow-up as
`issues 1665, 1666, 1667`, so mesh-delivery carried 1654 and 1665 as the pull request's issues and the Approve's
ask named them): mesh-delivery takes a pull request's issues from the words `issue <n>` and `issues <n>` in its
description (the plural yields its first number only), so write `issue 1608, issue 1646`, and name any other
issue in words, never by `issue <n>` or `issues <n>`.
**A catalog pull request that removes a module, or a subject a module names, is also judged by the
controller's unit tests against the catalog's branch**, because those tests read the catalog and no check
of the catalog 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 catalog's subjects until issue 1406 removed the name
mesh-issues still listened to. A controller's check does not find it, since a check reads the catalog's main
(also in a delivery group that holds the catalog's pull request); the lead runs mesh-controller's unit
suite with `MESH_CHECK_BESIDE` naming a directory that holds the catalog's branch, as mesh-controller's
`internal/beside` says.
2. **The checker's merge check runs the integration tests once, as the gate** (its one status, `mesh/check`). No
second review: step 2 reviewed this tree. 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 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
the same fixing agents; then push once again. **The failure is read from the check's whole log**, never the same fixing agents; then push **once** again (the one more push of ADR 0548). The check is **read, never re-asked to see if it passes**: a
check asked again without a new, reviewed head spends the build capacity for nothing. **The failure is read from the check's whole log**, never
reasoned from its summary: `mesh-delivery.show` names the check's build id, and `mesh-controller.builds` with reasoned from its summary: `mesh-delivery.show` names the check's build id, and `mesh-controller.builds` with
`log` set to that id prints everything the builder said. A check's summary picks only some failing lines; `log` set to that id prints everything the builder said. A check's summary picks only some failing lines;
on 2026-10-17 it showed a store test's `FAIL` without its cause, and a fix was first guessed from the code. on 2026-10-17 it showed a store test's `FAIL` without its cause, and a fix was first guessed from the code.
**A change to `merge-check.sh` is judged by main's version of it**, so a test the change adds must pass under **A failure the check hides or loses** (a verdict naming only the first failed test, a log not kept) is a fault of the
either version: one that runs the new script's behaviour runs a file of its own, never `merge-check.sh` itself. checking tool: it is fixed there, the same turn (instruction fix-a-bug-met-twice), not asked around.
**So a change that needs the new script to judge it ships the script first, as a small fix of its own**, and the **A change to the Go build module's recipe is judged by the build module the nodes run**, not by the changed one, so a
rest waits for its delivery: on 2026-10-17 milestone 3 of design 47 moved its integration tests behind a build test the change adds must pass under either version. **A change that needs the new recipe to judge it ships the recipe
tag in the same pull request as the script that runs that tag, and main's script would have skipped every first, as a small fix of its own**, and the rest waits for its delivery: on 2026-10-17 milestone 3 of design 47 moved its
tagged test, its acceptance tests among them. The prepare step looks for this: a change to `merge-check.sh` or to integration tests behind a build tag in the same pull request as the script that ran that tag, and main's script would
what it selects (build tags, the store-tests list) is cut ahead of the milestone. have skipped every tagged test, its acceptance tests among them. Since R4g no repository holds such a script (ADR 0529):
which tests run at which level is the build tag's (ADR 0445 §2), and hq's checks are Go tests like any repository's
(ADR 0527). The prepare step looks for this: a change to what the checker's build module selects (its recipe, a build
tag convention) is cut ahead of the milestone.
4. **The milestone's acceptance is proven by its check.** A milestone that adds or changes a module 4. **The milestone's acceptance is proven by its check.** A milestone that adds or changes a module
carries an integration test that starts the module whole, its process against the real bus and store, and 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 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). 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 first composition that meets it, 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 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; 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 an Approve at the terminal is issue 953), once its one check (`mesh/check`) passed, for a delivery group once for all its
members, naming the review record. On the Approve it merges the approved heads, a group's in one pass members, naming the review record. On the Approve it merges the approved heads, a group's in one pass
(ADR 0418). A new head ends the Approve and is asked again once its checks pass. A watcher follows the checks, (ADR 0418). A new head ends the Approve and is asked again once its checks pass, which is why a head is pushed only after its review. A watcher follows the checks,
the Approve and the merge, and wakes the lead. Beyond that the Approve and the merge, and wakes the lead. Beyond that
Approve, the operator's word is waited for only when the milestone changes what the operator sees, removes Approve, the operator's word is waited for only when the milestone changes what the operator sees, removes
data, or needs a step only the operator's terminal can take; the lead says which before the milestone starts. data, or needs a step only the operator's terminal can take; the lead says which before the milestone starts.
**A pull request that depends on an earlier one outside its delivery group is opened only once the earlier one is delivered**, its build
running on every machine it should reach, not merely merged. A note in the description is no safeguard: the
operator's Approve merges whatever is asked. On 2026-10-25 a second change merged before the first was delivered
and machines lost modules. How it is checked: `mesh-delivery.deliveries` shows the earlier change's source
revision `delivered` before the later pull request exists (`gitea.gitea_get_pull_request`, its creation time);
a later one older than that is this rule skipped. Members of one delivery group are the exception: they open together, with one check, one Approve and one merge in one pass (item 1).
**A reviewed follow-up to a change the operator has not yet approved is folded into that pull request** (its head
moved to include it): one Approve, one delivery. The old Approve ask closes itself on the new head and the new
head is asked once its checks pass. How it is checked: a follow-up that sits in a second pull request while the
first still waits shows in `gitea.gitea_list_pull_requests` as two open pull requests of one lead.
6. **A member pins mesh-sdk only at a commit that is a pull request's head**, merged or open (2026-10-28: the check
refused a pin to a merge commit on main as "on no pull request", though its tree equalled the head's). How it is
checked: the check's pin step; a refusal "on no pull request" is this rule skipped.
7. **Two groups that pin the same module's mesh-sdk conflict in `go.mod` and `go.sum`.** The group that merges
second merges main in and re-pins once, to a pull request head that holds both changes, after the first group
is merged and not before (a re-pin before makes the first group conflict). How it is checked: the check's merge
step; a group that conflicts after the first one merged is this rule skipped.
8. **A group that adds a seat, a verb or a manifest field in mesh-sdk and uses it in a module's manifest in the
same group cannot pass its check** until the running assigner pins a mesh-sdk that defines it, because the
check composes with the running assigner's mesh-sdk (issue 1585). The assigner's pin ships first, as its own
release. How it is checked: the check's composition step; a composition refusing the new word in such a group
is this rule skipped.
9. **A new head on an open pull request needs its `Reviewed-by` line at that head before the Approve is asked,
even for a pin-only commit.** How it is checked: the Approve's ask names the review record;
an ask whose line names another commit is this rule skipped.
**One exception: a catalogue change that uses a new manifest word.** The catalogue's merge check reads **One exception: a catalog change that uses a new manifest word.** The catalog's merge check reads
manifests with the controller the mesh runs, so the controller's pull request is merged and delivered first; manifests with the controller the mesh runs, so the controller's pull request is merged and delivered first;
then `mesh-delivery.recheck` the catalogue's and merge it on the new verdict. Such work is two releases, the controller's first, and playbook 04 cuts its milestones that way. then `mesh-delivery.recheck` the catalog's and merge it on the new verdict. Such work is two releases, the controller's first, and playbook 04 cuts its milestones that way.
**A new key the node-engine checks is such a word too** (issue 1051): the controller judges every machine with the **A new key the node-engine checks is such a word too** (issue 1051): the controller judges every machine with the
node-engine's own validator, vendored at the commit `go.mod`'s `replace` pins, so a key added to the node-engine is node-engine's own validator, vendored at the commit `go.mod`'s `replace` pins, so a key added to the node-engine is
refused by the gate until the controller's pin moves to the delivered node-engine (`go mod vendor`); the feature is refused by the gate until the controller's pin moves to the delivered node-engine (`go mod vendor`); the feature is
then three releases, the node-engine's, the controller's, the catalogue's. **Every release that must merge before then three releases, the node-engine's, the controller's, the catalog's. **Every release that must merge before
the catalogue's is on a branch name of its own** (`feat/<slug>-node-engine`, `feat/<slug>-controller`): on the the catalog's is on a branch name of its own** (`feat/<slug>-node-engine`, `feat/<slug>-controller`): on the
feature's name it joins the catalogue's delivery group, and a group merges only whole, so neither merges. feature's name it joins the catalog's delivery group, and a group merges only whole, so neither merges.
The exception ends when milestone 3 of design 47 is delivered (issue 977, ADR 0402, ADR 0403): the group's composed The exception ends when milestone 3 of design 47 is delivered (issue 977, ADR 0402, ADR 0403): the group's composed
check, judged by the group's controller, is then each member's gate, and the feature is one group. check, judged by the group's controller, is then each member's gate, and the feature is one group.
## Step 4 — Deliver ## Step 4 — Deliver
The merge starts the delivery plan: build, publish, send. A merge into a branch other than `main` is not a The merge moves the source revision; there is no delivery plan (design 47, ADR 0539). The indexer writes each
delivery: no machine receives it (issue 889 while it still starts one). The lead follows it to done module's new fingerprint, the builder builds it once, the controller writes its compositions, credential and grant,
(`mesh-controller.plans` with `wait` and `until: done`), then resolves the feature's issues with `fixed-by` and the assigner writes each node's want when its seven rules hold: the module's canary first, its other nodes once
naming its pull requests, and removes what is left: no `feat/<slug>` branch and no `.work/<slug>` worktree it runs healthy there. A merge into a branch other than `main` is not a delivery: no machine receives it.
survive.
**The view follows a delivery; nobody asks the controller.** The lead's watcher waits on the status view's events:
`moving` (a module's want on a node differs from its have), `settled` (the have reached the want, running and
healthy), `stuck` (held, a failed health check, or past the watchman's thirty minutes) and `delivered` (every want of
the source revision equals its fingerprint and every have its want). After each wake it reads the state again from
the view's wants page or its tools: `assigner_wants` and `assigner_want` (a want not yet at the newest fingerprint
names, in `newest` and `rule`, the rule that holds it back, and its words say why), and `node_haves` (the node
agent's haves: fingerprint, composition digest, phase and health). The delivery is done when the view says
`delivered` for its source revision; **"on the machines" from the delivery tools is not yet "delivered"**: the lead
also reads what the nodes run (the view's haves per node) and the indexer's record of the module (its newest
fingerprint) before saying a change is delivered. How it is checked: the report names each node's build.
Then the lead resolves the feature's issues with `fixed-by` 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 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 moved mesh-sdk with 41 dependents to all four machines. The lead reads the indexer's
candidates of the group (the view's `indexer_candidates`: each module whose fingerprint the change moves) and the
nodes that run them, and names what it moves before asking an Approve.
**A held move is the operator's answer, asked by the assigner.** A module of policy `record` whose move would
recreate a container or move data, a failed backup before an apply, and a data module whose new fingerprint failed
its health check on the canary each wait for the operator's answer to the assigner's ask; the view says them
`stuck`, and a condition names the node and the module. The lead reads the ask on the router
(`operator-channel.asks`) before telling the operator it waits, and adds it to the wait list. No session writes a
want to get past a hold.
**A new module's first assignment is its first real composition.** The composition check composes every machine as
it is assigned, so a module that no machine runs yet is composed by no check: on 2026-10-21 the build modules' first
assignment found two faults no merge check could see (issue 1537, a template the controller filled for a value the
forge serves only later; issue 1542, the node-engine refusing the module's own `.ssh`). So a new module is assigned
to one machine first, its have read after the node agent applies it (`node_haves`: phase and health) and its node's
answer read for a failed resource (`mesh-controller.node`), and taken back at once when that machine cannot compose
or apply, so the machine keeps applying everything else; the other machines follow once it runs there.
## Deliver the controller, the builder or a build module without the builder
The builder delivers its own module, the build modules and the controller from its keys, and nothing falls back to another builder
(ADR 0524 §7). When the builder or a build module cannot write references, or a controller cannot take its own fix (issue 1886: the
controller's plan could never finish, and no plan it made could deliver its own fix), the way is ADR 0526's, in this order, each
step recorded in the hand-act log with its reason:
1. **Revert, with no build.** Revert the change that broke the builder or the build module: the earlier source's fingerprint has a done key naming
references, so the revert's delivery builds nothing. Read first that the artifact store still holds the earlier artifact
(`mesh-controller.artifacts`): it keeps a recorded build while it is among its module's five newest or a definition names it, and the
controller keeps its composition while it is among the five newest too, so the assigner's want delivers it.
2. **A build by a session's call.** Call the build module's fetch and build tools on one named build node (for Go, `build_go_fetch` and then
`build_go_build` with `publish: true`); the build module pushes the artifact and answers its reference. `mesh-pack` in mesh-sdk gives a
hand build the build module's form; write the Go version of each hand build down, since another version gives another digest.
3. **Register by reference, at the controller's terminal only.** The operator names the module, its repository, its path and each artifact's
reference there; no verb reaches it. Say before you ask that this is a terminal step, and which notifications it raises; check that the
command exists on the node before you hand it over.
4. **A controller that cannot take its own fix** is put back by keys the operator writes at the terminal, with the operator's administrator
account (the operator's credential), never by a declaration sent by hand (ADR 0539 §10). To an earlier controller: the operator writes
`want.<control-node>.mesh-controller` naming a fingerprint whose composition is kept and that composition's digest; a controller
composes the one it replaced at its start, so that one is always kept. To a hand build never composed: register it as in step 3, then
`mesh-hand-compose` (in mesh-sdk, beside `mesh-pack`) composes the module for the control-node from the controller's kept inputs, signs it
with the operator's key, and with `-write` and the operator's credential writes the composition into `composed-<node>` and its digest
into `mesh-controller_composed`; the operator then writes the want naming that digest. Read the composition `mesh-hand-compose` prints
before `-write`. Each act goes into the hand-act log with its reason; say before you ask that these are terminal steps, and which
notifications they raise.
5. **Before a switch that moves the controller, the builder or a build module,** the lead keeps the running versions' artifacts outside the
artifact store, by digest on two nodes, with module, artifact, commit, Go version and the steps that put each back; the store's nightly
collection removes a blob the controller did not record.
Nothing here is a work-around for a delivery that can run normally: while the builder delivers, no hand act replaces it.
## Models ## Models
@@ -271,63 +527,69 @@ on, with when it started and its bound.
|---|---|---| |---|---|---|
| a fixing agent's issue | 30 minutes | ask it where it stands; split or take over | | 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 | | 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 check stream's backlog and the age of its oldest ask are in the delivery view (ADR 0525 §9) |
| 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 | | the Approve's ask | 5 minutes after the check (`mesh/check`) passes | 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 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` | | a delivery | its class's budget (3 minutes for a leaf module, 10 for a core module), at most 30 minutes | read the view's wants page, or `assigner_wants` against `node_haves`: a want held back names its rule, a `stuck` module its words |
**Every wait on the list has a watcher** (the operator, 2026-10-17). A session runs only when a message reaches **Every wait on the list has a watcher** (the operator, 2026-10-17). A session runs only when a message reaches
it, so a check, a merge, a delivery plan, a record's review or the operator's Approve on a channel that lands it, so a check, a merge, a delivery, a record's review or the operator's Approve on a channel that lands
while the lead's turn has ended wakes nobody: on 2026-10-17 an approved group sat half merged for 14 minutes 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, 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). never the smallest (the operator, 2026-10-17).
It loops `mesh_wait` on the event families of what it watches (`mesh-delivery.transition` and It loops `mesh_wait` on the event families of what it watches (`mesh-delivery-gate.transition` and
`mesh-delivery.group` for a delivery, for example) with a timeout under 60 seconds, since the call itself ends near `mesh-delivery-gate.group` for a merge, `mesh-delivery-view.moving`, `.settled`, `.stuck` and `.delivered` for its
delivery to the nodes, 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`, 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 view's `assigner_wants` and `node_haves`, `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. 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 delivery: 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 **The cadence is the instruction section "report-cadence"** (the operator, 2026-10-28; it replaces the full layout every
needs the operator,** the lead reports, in one of the two layouts below (the operator, 2026-10-16: an overview five minutes of "how-work-is-done"). Times are local and absolute, from the clock (`date`), never guessed; every number
of the issues, often). Times are local and absolute, from the clock (`date`), never guessed; every number has its has its unit and its bound or baseline; an issue is named by its number and a few words, never by its number alone.
unit and its bound or baseline; an issue is named by its number and a few words, never by its number alone.
**The full layout** (in the session) is the whole design as a tree (the operator, 2026-10-17: always the full - **Every five minutes** (ten until 2026-10-26, five since), every two minutes while a delivery moves (the view says
overview): every milestone of the design with its state, and under the running milestone its repositories and modules `moving`), and at once when something lands, stops or needs the operator, the lead sends the **short
their issues, each issue under the one it waits for, marked ✓ landed or delivered, ▶ fixing, ⏸ waiting, ✗ stopped: report**: the tree exactly as `knowledge-base.progress` with `of: "design <nn>"` and `detail: minimal` answers it,
unedited, with the verb's read-at time, then at most three lines: what changed, what needs the operator, what is next.
Once the verb takes `since`, the short report asks `since: 5m`.
- **About every thirty minutes**, and whenever something stops, the lead sends the **detailed report**: the same verb
with `detail: detailed` (or `since: 60m`), then the full layout below.
- **Every report ends by scheduling the next** (a timer or a watcher set ahead), so that agents' incoming reports
never replace it.
- **An answer to the operator between reports is short and has no tree.**
**The tree is the verb's answer and is never drawn or corrected by hand.** `knowledge-base.progress` answers the
design's milestones, issues, pull requests and held groups in one call, each with when it last moved (issue 1947).
What it cannot read for certain from the design's milestone table it says in `warnings`; the lead fixes the table, it
does not correct the tree by hand.
**The full layout** (the detailed report) is the verb's detailed tree followed by these lines:
``` ```
Feature <slug> · milestone <n> of <m> · step <name>, <minutes used> of <bound> min Opened along the way: <each issue opened during the feature, its number and a few words, and when it is fixed: in between, after the delivery, or waits on what> — or "none"
Issues: <landed> of <total> landed · <fixing> fixing · <waiting> waiting
Design <nn> · <its title>
├── ✓ milestone 1 <its outcome, a few words> delivered 12:58
├── ▶ milestone 2 <its outcome, a few words>
│ ├── mesh-controller (agent: fixing, 2 of 3 landed)
│ │ ├── ✓ 652 assignments on the bus landed 14:02
│ │ ├── ▶ 736 placement words leave the layers fixing since 13:51, 23 of 30 min
│ │ │ └── ⏸ 751 config keys named (mesh-catalog) waits for 736
│ │ └── ✓ 654 a file names its template landed 13:20
│ └── mesh-catalog (agent: waiting)
│ └── ⏸ 751 (above, under 736)
└── ⏸ milestone 3 <its outcome, a few words> <its issues>
Opened along the way: <each issue opened during the feature, its number and a few words, and where it goes> — or "none"
Past its bound: <the wait, how long, what is being done> — or "none" Past its bound: <the wait, how long, what is being done> — or "none"
Needs you: <one line per question, with my recommendation> — or "nothing" Needs you: <one line per question, with my recommendation> — or "nothing"
Allowance: week <n> % (<+d> since the last report), session <n> % Allowance: week <n> % (<+d> since the last report), session <n> %
Worth it: <what in the last stretch served the goal, and what did not>
Next: <the next actions up to the delivery, in order> Next: <the next actions up to the delivery, in order>
``` ```
**The short layout** (on a channel such as Telegram, or when nothing changed but time): How it is checked: the operator sees a short report at least every five minutes and a detailed one at least every
thirty; a report's tree that differs from the verb's answer of the same minute is a tree assembled by hand, a report
without the verb's read-at time is the same, and a report that does not end by scheduling the next is a breach the
lead reports.
``` On a channel such as Telegram the same short report is sent, the tree as a code block.
<slug> milestone <n>/<m> · <step> <used>/<bound> min · <landed>/<total> landed · past bound: <none | what> · needs you: <nothing | what> · week <n> %
```
A report says what changed since the last one first. In the session every report is the full layout, the whole
tree of the milestone's issues included, also when only time passed: the operator reads the state from it without
asking. The short layout is for a channel.
## The context of the lead's own session ## The context of the lead's own session
@@ -346,7 +608,7 @@ moments, the same hour, an hq change going through [playbook 08](08-small-fixes.
- **The first milestone starts:** the design's `status: in-progress` ([playbook 04](04-build-handoff.md) step 7), - **The first milestone starts:** the design's `status: in-progress` ([playbook 04](04-build-handoff.md) step 7),
if it is not already. if it is not already.
- **A milestone's delivery plan is done:** its issues are resolved (step 4), with `fixed-by` naming its pull - **A milestone is delivered** (the view says `delivered` for its source revision): its issues are resolved (step 4), with `fixed-by` naming its pull
requests. The design's milestone entry is not changed: it names the milestone's issues, and whether they are requests. The design's milestone entry is not changed: it names the milestone's issues, and whether they are
delivered, when and by what is read from the issue tracker when it is asked, never copied into the design delivered, when and by what is read from the issue tracker when it is asked, never copied into the design
(issue 1038; AGENTS.md: cross-cutting views are generated when they are read). (issue 1038; AGENTS.md: cross-cutting views are generated when they are read).
@@ -406,6 +668,16 @@ issues in `fixing` that nobody works on reads to the operator as stuck work.
- Every review record of a release names one reviewer (`mesh-delivery.show`, the review record), and the lead's - Every review record of a release names one reviewer (`mesh-delivery.show`, the review record), and the lead's
save point names one reviewer id per release; a second reviewer within one release is a reviewer not kept save point names one reviewer id per release; a second reviewer within one release is a reviewer not kept
(issue 1042). A pull request whose ask says it has no review record at its head is a record not written. (issue 1042). A pull request whose ask says it has no review record at its head is a record not written.
- A running milestone's file holds the three answers of step 0 item 8 (`cat .work/<slug>/milestone.md`), and the
review report says it read them (step 2, item 6a): a bucket key or stream in the diff (`git diff main --
| grep -i 'bucket\|stream'`) that the lifetime answer omits is the failure of that item (research 071).
- A pull request merged by hand in a temporary mode names, in its description on the forge (`gitea.gitea_get_pull_request`), the
verdict of its one check; a try with tests `.` is named only where the operator asked for one.
- **One release, one check** (ADR 0548): `gitea.gitea_get_pull_request` shows each pull request created after its
review record's time, and `mesh-delivery.show` shows one check per head and, for a group, one head per member; a pull
request that existed before its review approved it, a second head with no failed check before it, or a check of a
head asked more than once, is this rule skipped. A call of `mesh-controller.try`, `mesh-delivery-checker.try` or `checker_try` that the operator
did not ask for shows in the checker's asks and is the same breach.
- A milestone that adds or changes a module whose pull request has no integration test that starts the module - A milestone that adds or changes a module whose pull request has no integration test that starts the module
whole is the failure of the check step's fourth item. whole is the failure of the check step's fourth item.
- A bed run from `.work/<slug>/mesh-lab` that fails naming a `.work/<slug>/<repo>/…` path it cannot find is a - A bed run from `.work/<slug>/mesh-lab` that fails naming a `.work/<slug>/<repo>/…` path it cannot find is a
+68 -21
View File
@@ -16,52 +16,92 @@ hour is a feature.
**Who runs it.** Anyone who writes code, engineers and agents alike. **Who runs it.** Anyone who writes code, engineers and agents alike.
**A fix found during a milestone rides in that milestone's group** (the operator, 2026-10-21, after a day with ten
mesh-sdk merges, each rebuilding about forty dependent modules, and eight mesh-controller merges, each sending a
delivery to every node). Playbook [07](07-feature-branches.md) makes a milestone one piece of functionality
released once as one delivery group (ADR 0482). A small fix in a repository the running milestone touches, mesh-sdk
and mesh-controller above all, is built as below but on the milestone's branch, so its pull request joins the group
and merges with it. The exception is a fault that is broken for the operator now (something they see failing at
this moment): it goes alone as a small fix, and the lead's report says why.
## The aim ## The aim
A small fix spends its time on the fix, and runs the long part once: **one review, one push, one check run, 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. one delivery** (ADR 0548; the operator, 2026-10-26: "every release should only run 1 check or 1 try"; "we don't have more
processing power"). Everything before the push is local and fast, and no try is run.
## Steps ## Steps
1. **Read the issue and the code it names** (the session's own model, or one read-only agent on the smaller 1. **Read the issue and the code it names** (the session's own model, or one read-only agent on the smaller
model answering exact questions with file:line). If a design question is open, it is settled before any model answering exact questions with file:line). If a design question is open, it is settled before any
code is written: on the issue, through the knowledge base, or by the operator. A fix that turns out to code is written: on the issue, through the knowledge base (`knowledge-base.ask` first: what is decided is read before anything is designed), or by the operator. A fix that turns out to
need a second repository, or more than about an hour, stops here and becomes a feature; one that needs a need a second repository, or more than about an hour, stops here and becomes a feature; one that needs a
decision stays a small fix. **The session's own model (the larger) then writes the issue's acceptance decision stays a small fix. **The session's own model (the larger) then writes the issue's acceptance
criteria** as playbook [07](07-feature-branches.md) step 0 item 5 says: each one sentence, given, when, then, criteria** as playbook [07](07-feature-branches.md) step 0 item 5 says: each one sentence, given, when, then,
naming the level of the test that proves it, as an `issue-tracker.note` headed `Acceptance criteria`. A naming the level of the test that proves it, as an `issue-tracker.note` headed `Acceptance criteria`. A
playbook or design edit has none: its check is `sh merge-check.sh`. playbook or design edit has none: its check is `go test ./...` at hq's root, which runs the four checks (ADR 0527).
2. **Move the issue to `fixing`** with `by` as who@node. 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 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 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). before every commit (each test well under a second; never a merge check or a database locally: how-we-build §5, ADR 0391).
Check that it compiles with `go vet`, never `go build` in a module's folder: with one main package it writes the
program beside `go.mod`, and `git add -A` takes it (issue 1577: two 5-10 MB programs committed on 2026-10-21,
caught only by the review); read `git show --stat` before pushing.
The tests are written from the acceptance criteria, at least one per criterion at its level, each citing the 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, 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`). `go test ./...` at the root). The integration tests are compiled (`go vet -tags integration ./...`) and not run:
4. **One review of the local commit, before anything is pushed** (a reviewer that did not write it, larger nothing is pushed, no pull request is opened and no try (`mesh-controller.try`, `mesh-delivery-checker.try`, `mesh-delivery-checker.checker_try`) is called, unless the operator asks for
one. A single integration test may be run locally against an embedded bus when it is the fastest way to find a cause;
before the push, the integration tests that exercise the change are run that way, by name
(playbook 07 step 3), and the pull request names them and their results. A test fixture writes state in the form its real
writer writes it; the review asks it.
4. **One review of the local commit** (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 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 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 criteria is opened as an issue of its own, never asked as a change; it judges the integration tests by reading them against the code, as playbook 07 step 2 item 4 says; it answers playbook 07 step 2 item 10's two questions, on state and seams and on mesh-agnostic repositories, in its report). 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 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. 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 once and open the pull request, only after the review approves.** 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 (ADR 0401), so a fix waits for no other change's delivery. No pull request, draft or not, exists before the review
the integration tests once, and the pull request is opened only on a pass; the forge's merge check runs approves the head, and no fix round is pushed to an open pull request: every push to an open pull request asks a new
them again as the gate (playbook [07](07-feature-branches.md) step 3). They are never run on the machine. A check. The checker's merge check runs the unit and the integration level once, as the gate (playbook [07](07-feature-branches.md) step 3); the pushed head is read from the forge first, as playbook 07 step 3 item 1 says.
failed try or check goes back to the same agent with the failure, and the reviewer reads only the change it makes. A failed check is read from its whole log (never re-asked to see if it passes), then the repository's whole integration level
is run locally (`go test -tags integration -count=1 ./...` in each module) so every failure shows at once, where a verdict
names only the first (issue 1921); the failures go back to the same agent, fixed in one change, the reviewer reads only the change
it makes, and the head is pushed once more.
**A temporary mode that merges by hand** (the lead merges because the normal Approve and merge path is not
running): the one check still runs, and the lead names its verdict in the pull request's description; a try with tests `.` is asked only when the operator asks for one (research 071; playbook
[07](07-feature-branches.md) step 3).
6. **The pull request's description carries the review record** (`Reviewed-by: <who@node> at <full sha>`, 6. **The pull request's description carries the review record** (`Reviewed-by: <who@node> at <full sha>`,
playbook [07](07-feature-branches.md) step 3), and mesh-delivery asks the operator's Approve as soon as its playbook [07](07-feature-branches.md) step 3) and the session that pushed it (`Pushed-by: <who@node>`, ADR 0505 §5), and mesh-delivery asks the operator's Approve as soon as its
checks pass and merges the approved head (ADR 0421); nobody marks, labels, comments or rechecks. A fix to a module that changes how it runs carries an checks pass and merges the approved head (ADR 0421); nobody marks, labels, comments or rechecks. A fix to a module that changes how it runs carries an
integration test starting the module whole against the real bus and store, run by `mesh-controller.try` and the forge's checks integration test starting the module whole against the real bus and store, run by the checker's one merge check
(ADR 0391 §1, §6); unit tests alone are not that proof, and the lab is not used. The operator's word is waited for only when the fix changes what the operator sees, removes data, (ADR 0391 §1, §6); unit tests alone are not that proof, and the lab is not used. The operator's word is waited for only when the fix changes what the operator sees, removes data,
or needs the operator's terminal. or needs the operator's terminal.
7. **Follow its delivery plan to done** (`mesh-controller.plans` with `wait` and `until: done`), prove the fix on **A fix that depends on another pull request outside its delivery group opens only once that one is delivered**, not merely merged (members of one group open together, as playbook 07 step 3 item 1 says) (a note
the mesh (the symptom gone, measured), and resolve the issue with `located-in` and `fixed-by` the same hour. in the description does not hold it back: the Approve merges whatever is asked; 2026-10-25, machines lost modules
when a second change merged first). **A reviewed follow-up to a fix still waiting for the Approve is folded into
that pull request** (head moved): one Approve, one delivery. Both are checked as playbook
[07](07-feature-branches.md) step 3 says.
**At the second instance of a fault kind found in a review, the developer searches for every instance** and names
each in the report (playbook [07](07-feature-branches.md) step 2, item 9).
7. **Follow its delivery to done**: a watcher waits on the status view's events (`mesh-delivery-view.moving`,
`.settled`, `.stuck` and `.delivered`) and reads the view's `assigner_wants` and `node_haves` after each wake, as
playbook [07](07-feature-branches.md) step 4 says; the delivery is done when the view says `delivered` for its
source revision, and the haves per node and the indexer's record of the module name the build (playbook 07 step 4;
"on the machines" alone is not "delivered"). Then prove the fix on the mesh (the symptom gone, measured), and resolve the issue with
`located-in` and `fixed-by` the same hour.
A fix that stops before its delivery moves its issue back to `open` with a note saying how far it got A fix that stops before its delivery moves its issue back to `open` with a note saying how far it got
(branch, last commit, what is left), as playbook [07](07-feature-branches.md) says. (branch, last commit, what is left), as playbook [07](07-feature-branches.md) says.
**An issue opened along the way is fixed too** (the operator, 2026-10-21). A fix's review, check or delivery that
opens an issue (a remark outside the done-when, a fault met live) leaves it to the same session to fix: right after
this fix's delivery, as a small fix of its own, or folded into the next one in the same repository. It waits only
when another running milestone owns it, or it needs a decision or the operator first; the report then says on what.
**A second fault found by the delivery.** When a fix's delivery shows a second fault that keeps the issue's **A second fault found by the delivery.** When a fix's delivery shows a second fault that keeps the issue's
done-when from holding, and that fault is in the same files, it is taken into the same issue as a second pull done-when from holding, and that fault is in the same files, it is taken into the same issue as a second pull
request with a review round of its own. A fault in other code is opened as an issue of its own, and the first request with a review round of its own. A fault in other code is opened as an issue of its own, and the first
@@ -82,10 +122,10 @@ code.
|---|---|---| |---|---|---|
| building the fix | 1 hour | it is a feature: stop and plan it under playbook 07 | | building the fix | 1 hour | it is a feature: stop and plan it under playbook 07 |
| the review | 15 minutes | ask the reviewer where it stands | | the review | 15 minutes | ask the reviewer where it stands |
| the merge check | 15 minutes | read `mesh-controller.queue` and the check's log | | the merge check | 30 minutes (it waits behind the asks before it for a build node's free slot, ADR 0525) | read `mesh-delivery.show`, the check's log and the check stream's backlog in the delivery view |
| 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 | | the Approve's ask | 5 minutes after the check (`mesh/check`) passes | read `mesh-delivery.show`: an ask not raised is a fault, said to the operator |
| the merge after the Approve | 5 minutes | read `mesh-delivery.show` for the delivery | | the merge after the Approve | 5 minutes | read `mesh-delivery.show` for the delivery |
| the delivery plan | its class's budget (`mesh-delivery.times`), at most 30 minutes | read `mesh-controller.plans`, then `mesh-delivery.stalled` | | the delivery | its class's budget (3 minutes for a leaf module, 10 for a core module), at most 30 minutes | read the view's wants page, or `assigner_wants` against `node_haves`: a want held back names its rule, a `stuck` module its words |
A wait past its bound is said to the operator at once, with what is being done about it. A wait past its bound is said to the operator at once, with what is being done about it.
@@ -97,8 +137,12 @@ reading itself and uses one subagent only for the review.
## How it is checked ## 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 - 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 merged by hand in a temporary mode names the verdict of its one check in its description
on the forge (`gitea.gitea_get_pull_request`); one without is the step skipped.
- One fix, one check (ADR 0548): a pull request that existed before its review approved the head, or a head whose check was
asked more than once with no new reviewed head, is steps 4 and 5 skipped; so is a `mesh-controller.try` or `mesh-delivery-checker.try` the operator did not ask for.
- Its issue moves to `resolved` within the hour the view says its source revision `delivered`.
- A pull request with no review record at its head (the Approve's ask says so first), a fix to a module's - 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 running pushed with no integration test of the module whole, or a review report
asking a change outside the issue's done-when and acceptance criteria, is the failure of steps 4 to 6. asking a change outside the issue's done-when and acceptance criteria, is the failure of steps 4 to 6.
@@ -107,5 +151,8 @@ reading itself and uses one subagent only for the review.
(`git grep -n '<issue>/AC<n>'`); one without is step 1 skipped. (`git grep -n '<issue>/AC<n>'`); one without is step 1 skipped.
- An issue in `fixing` with nobody on it shows in `issue-tracker.list` with `flag: abandoned`; a stopped fix - An issue in `fixing` with nobody on it shows in `issue-tracker.list` with `flag: abandoned`; a stopped fix
moves it back to `open` with its note. moves it back to `open` with its note.
- A fix found during a milestone is named in the lead's report with the group it rides in, or with the reason it
went alone. `mesh-delivery.deliveries` shows the milestone's members in one group, merged as one source revision
per milestone; a merge of mesh-sdk or mesh-controller outside the group, with no reported reason, is this rule skipped.
- The repository's own clone says `main` (`git -C <clone> branch --show-current`); another branch there is a - The repository's own clone says `main` (`git -C <clone> branch --show-current`); another branch there is a
fix built outside a worktree (issue 1010). fix built outside a worktree (issue 1010).
+66
View File
@@ -0,0 +1,66 @@
# Playbook 10 — Visual design
**Trigger.** A page or a screen the operator will use is about to be designed or rebuilt, or the operator judges
one unusable. It runs before the decisions about its look are written and before any of its code: a page built
first and judged after it ships is rebuilt (the operator, 2026-10-22, on design 59's mail page, judged unusable
the day it was delivered).
**Who runs it.** The lead of the design, on the larger model. Mockups may be drawn by an agent on the smaller
model from the lead's brief. The operator picks; nobody else does.
## What it produces
- **The reference**: one mockup the operator picked, kept with the design as `NN-mockup/` beside its to-be
document. It is a direction, not a pixel specification: the build matches what it shows the operator doing,
in the order and the weight it shows, not its exact pixels.
- **The decisions it needs**: what the pick changes about what the mesh keeps, shows or does, written as decision
records (playbook [02](02-graduation.md)) before the design's milestones (playbook [04](04-build-handoff.md)).
- **An acceptance test in the operator's words**: the page is done when the operator uses it, on the devices they
named, for the work the mockup shows.
## Steps
1. **Gather what the page must carry.** A page whose direction needs research first starts at playbook
[01](01-research.md); this playbook then draws it. Read the as-is of what it replaces and the real shape of its data from the
mesh (how many items, how long a line, what is missing in practice), the operator's complaint in their own
words, and a short survey of how others solve it (a handful of products, one line each on what changes for
the person). Write it into the work folder; nothing goes on the mesh.
2. **Draw three to five mockups with fixed data.** Each is one static HTML file that opens from disk: invented
data shaped like the real (names, amounts, lengths, a long subject, an empty sender), no backend, a mock script
only where an interaction is the point. Each mockup is a different direction, not a variation of one; each
shows a phone's width and a desktop's. One index page links them all.
3. **Show them and ask one question.** Put the index on the machine the operator is at, and ask which direction to
keep, one question at a time with the lead's recommendation. A round's answer may be a pick, a combination of
two, or a change; the next round draws only that. Three rounds without a pick mean something the operator wants
is missing: ask what, before drawing again.
4. **Make the pick the reference.** Copy it into the design's `NN-mockup/`, with every name, address, employer,
mail address, domain, provider and amount of the operator's own replaced by invented ones: hq is public. Say in the design that
the reference is a direction, and link it.
5. **Write what is new as decisions,** each with the operator's Approve where it changes what they see, then the
design and its milestones. The design's acceptance tests end with the operator using the page live.
6. **Hold the build to it.** The lead gives playbook [07](07-feature-branches.md)'s reviewer the reference with
the built page, at a phone's and a desktop's width, as part of what the review reads against the issue. The
operator using the page is the milestone's live proof, made before the next milestone starts (playbook
[04](04-build-handoff.md)).
## Bounds
| Waiting on | Bound | Past the bound |
|---|---|---|
| a round of mockups | 1 hour | say to the operator what is taking long |
| the operator's pick | the next exchange | ask once more, one question, with the recommendation |
| rounds without a pick | 3 | ask what is missing before drawing again |
## Rules
- No code reaches the mesh during a visual design: mockups live in the work folder until one is picked.
- Mockups use invented data, never a copy of the operator's mail, files or accounts.
- The operator's pick stands over the lead's taste; the lead's recommendation is said once, with its reason.
## How it is checked
- A to-be design of a page the operator uses links its reference mockup in `NN-mockup/`, and that mockup holds
none of the operator's own names, addresses, domains, providers or amounts (read by the reviewer of the design's pull request).
- The milestone's review read the built page beside the reference at a phone's and a desktop's width (its
report says so).
- The design's last acceptance test is the operator using the page, proven before the next milestone starts.
+523
View File
@@ -0,0 +1,523 @@
<!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, amended 2026-10-26 for ADR 0548 (one release, one check: no try).</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">wants and haves</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. Nothing is pushed or tried before the review (ADR 0548): the branches are pushed <b>once, after the review approves</b>, and the one check runs the integration tests.</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</text>
<text x="410" y="34" text-anchor="middle" class="mute">30 min per issue · local only</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">push once · 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">until delivered</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">the one check, wants</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</text>
<text x="410" y="96" text-anchor="middle" class="mute">each branch, local</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">once, 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">reads tests · 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">no try</text>
<text x="410" y="312" text-anchor="middle" class="mute">nothing pushed yet</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">one check · mesh/check</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 · wants</text>
<text x="1000" y="312" text-anchor="middle" class="mute">backups first, canary first</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="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>Every part the change reaches is an issue of the milestone; their order is the assigner's rules', never a cut.</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) are compiled there and never run: the checker's one merge check, asked by the one push after the review, runs them as the gate. There is no try (ADR 0548).</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="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">push once</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 + 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 by reading it: no run exists before the check.</li>
<li>That each integration test enters by the path the live trigger takes.</li>
<li>Where each piece of state lives and how each seam travels: anything but bus state, a stream or a seat's verb is a finding, even if a record put it there.</li>
<li>That no repository holds this mesh's setup (node names, placement, settings): repositories are mesh-agnostic.</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. ADR 0548 and playbook 07 step 2 item 4 change the last: bus tests are judged by reading them against the code, and the one check runs them after the review.</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; the records test 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; the records and index tests; 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; the cycle test 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 functionality, never by an order between parts; 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)","Answer three questions in the milestone file: who deletes each new key and stream, how the switch-over is tried on one node first, what the one check does not cover. The review checks them. (new)","List every part the change reaches; 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; nothing is pushed, no try runs. (new)","Review all of it, reading the integration tests against the code; at most 3 rounds; the report answers the state-and-seams and mesh-agnostic questions. (new)","A group pins the group's own mesh-sdk head in every member. (new)","Push the reviewed heads once and read each forge head against the reviewed commit; no draft pull request, no fix round on an open one. (new)","Open one pull request per repository, together, with Reviewed-by and Pushed-by; the one check runs. A failed check is read, fixed, reviewed, pushed once more. (new)","The operator's Approve on Telegram; the group merges in one pass.","Follow the delivery in the status view (moving, settled, stuck, delivered) to done; resolve the issues; clean up. (new)","When the builder cannot deliver its own fix: revert, a build by a session's call, a registration at the terminal, copies kept outside the store; a broken controller is put back by a want the operator writes, a hand build by mesh-hand-compose and a want. (new)"],
prod:"One pull request per repository, one source revision delivered, 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 check, pushed once",
prev:["Pull requests as scratchpads","Context bought again and again","Review rounds spent on what a run would show","A node agent's bus reads, proven only by review"],
check:"The checker's one status, mesh/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)","Nothing is pushed and no try runs before the review. (new)","One review of the local commit.","Push the reviewed head once; only then open the pull request. (new)","The operator's Approve; the merge; the delivery, followed in the status view. (new)","Prove the fix live and resolve the same hour.","During a milestone: ride in its delivery group, unless broken for the operator now. (new)"],
prod:"One pull request, one delivery, a resolved issue.",
next:"—",bounds:"About an hour · one review, one push, one check",
prev:["A fix reviewed after its pull request opened","A long part run twice","A fix waiting on someone else's delivery","Ten SDK merges and eight controller merges in a day, one per fix"],
check:"The checker's status; the review record; the tracker's move; one delivery group and one source revision per milestone."},
{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."},
{n:"10",name:"Visual design",kind:"idea",c:"--idea",
trig:"A page the operator will use is about to be designed or rebuilt, or the operator judges one unusable.",
who:"The design's lead; the operator picks.",
steps:["Gather what the page must carry: the as-is, the real data's shape, the operator's words, how others do it.","Draw three to five mockups with fixed, invented data, phone and desktop. (new)","Show them; ask one question per round. (new)","Make the pick the reference, with the operator's own details replaced. (new)","Write what is new as decisions, then the design and its milestones.","Hold the build to the reference; done when the operator uses it."],
prod:"The reference mockup beside the design, its decisions, an acceptance test of live use.",
next:"02, then 04",bounds:"A round 1 h · 3 rounds without a pick: ask what is missing",
prev:["A page judged unusable the day it ships","Rebuilding after delivery"],
check:"The design links its mockup; the review shows the build beside it; the operator's use is the last test."}
];
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>
+2 -2
View File
@@ -14,7 +14,7 @@ and a forge address is an operational detail (see [`README`](../README.md)).
| Repository | Owns | | Repository | Owns |
|---|---| |---|---|
| `hal` | The monorepo — the node runtime, the module catalogue, the delivery machinery, and the bootstrap scripts. Every core module lives here. | | `hal` | The monorepo — the node runtime, the module catalog, the delivery machinery, and the bootstrap scripts. Every core module lives here. |
| `hq` | This repository, under the company organisation — mission, research, design, decisions, issue diagnosis. Company-scoped (ADR 0019); the mesh is its first product. The source of truth for *why*. Carries no implementation. | | `hq` | This repository, under the company organisation — mission, research, design, decisions, issue diagnosis. Company-scoped (ADR 0019); the mesh is its first product. The source of truth for *why*. Carries no implementation. |
| *(one per application)* | Every standalone application, site or side-project gets its own repository, with `module.yml` at the root. Registered with the mesh as a build source; built and deployed by the same pipeline as anything in the monorepo. | | *(one per application)* | Every standalone application, site or side-project gets its own repository, with `module.yml` at the root. Registered with the mesh as a build source; built and deployed by the same pipeline as anything in the monorepo. |
| `migration` | **Private.** The record of one installation replacing the predecessor mesh with this one: the runbook, a dated log of every step and what it cost, the per-service data procedures, the readiness checks, and the scripts. Private because it is the opposite of this repository in every way that matters — it names machines, addresses, ports and paths, because a procedure that cannot be followed is not one. Where hq asks *what did we decide and why*, that repository answers *what happened on the machines, in what order, and what to do next*. Its `HANDOFF.md` is where somebody picking the work up starts. | | `migration` | **Private.** The record of one installation replacing the predecessor mesh with this one: the runbook, a dated log of every step and what it cost, the per-service data procedures, the readiness checks, and the scripts. Private because it is the opposite of this repository in every way that matters — it names machines, addresses, ports and paths, because a procedure that cannot be followed is not one. Where hq asks *what did we decide and why*, that repository answers *what happened on the machines, in what order, and what to do next*. Its `HANDOFF.md` is where somebody picking the work up starts. |
@@ -48,7 +48,7 @@ Named by role, because the layout is itself part of the as-is design — see
| Area | Holds | | Area | Holds |
|---|---| |---|---|
| Module catalogue | One directory per module, each with a manifest. Core modules sit under the mesh's own namespace; everything else at the top level. | | Module catalog | One directory per module, each with a manifest. Core modules sit under the mesh's own namespace; everything else at the top level. |
| Node runtime | The daemon and interactive runtime that every node runs. | | Node runtime | The daemon and interactive runtime that every node runs. |
| Bootstrap scripts | First-node initialisation, joining an existing mesh, and node rescue. | | Bootstrap scripts | First-node initialisation, joining an existing mesh, and node rescue. |
| Shared library | The SDK every module builds against. | | Shared library | The SDK every module builds against. |
+1 -1
View File
@@ -14,7 +14,7 @@ the commit the knowledge base's records name as their source (issue 426).
This folder keeps one thing: the six topics of the reading order, in the order somebody would learn the This folder keeps one thing: the six topics of the reading order, in the order somebody would learn the
system. Each record names its own topic, and the knowledge base reads these lines through the records module system. Each record names its own topic, and the knowledge base reads these lines through the records module
and refuses a record whose topic is not one of them. `index.py` checks that they are written. and refuses a record whose topic is not one of them. the index test (`00-META/checks/`) checks that they are written.
1. **What the mesh is** — `topic: the mesh`. What it is for, and the parts it is made of. 1. **What the mesh is** — `topic: the mesh`. What it is for, and the parts it is made of.
2. **Its tiers, from the bottom up** — `topic: the tiers`. The foundation, the node and the controller, and 2. **Its tiers, from the bottom up** — `topic: the tiers`. The foundation, the node and the controller, and
+1 -1
View File
@@ -11,7 +11,7 @@ decisions:
# The mesh as it stands # The mesh as it stands
A set of machines, each running the same runtime, each loading only the parts of the catalogue A set of machines, each running the same runtime, each loading only the parts of the catalog
it has been assigned. They hold no shared filesystem and make no direct connections to one it has been assigned. They hold no shared filesystem and make no direct connections to one
another. What makes them a mesh is a database that knows what should run where, and a message another. What makes them a mesh is a database that knows what should run where, and a message
broker that carries everything between them. broker that carries everything between them.
@@ -105,9 +105,9 @@ the resource rather than on the node that built the module.
## A documented rule with no enforcement ## A documented rule with no enforcement
Every module exposing capabilities is documented as required to declare the mesh's core runtime Every module exposing capabilities is documented as required to declare the mesh's core runtime
as a dependency. **Zero of the catalogue's modules do.** as a dependency. **Zero of the catalog's modules do.**
This is recorded here rather than quietly corrected, because it is the clearest instance of the This is recorded here rather than quietly corrected, because it is the clearest instance of the
rule this repository states about itself: a rule whose enforcement does not exist is rule this repository states about itself: a rule whose enforcement does not exist is
indistinguishable from a wrong one, and costs more, because people believe it. Whether the rule indistinguishable from a wrong one, and costs more, because people believe it. Whether the rule
or the catalogue is wrong has not been decided. or the catalog is wrong has not been decided.
+104 -59
View File
@@ -1,12 +1,11 @@
--- ---
layer: as-is layer: as-is
status: implemented status: implemented
code: [mesh-controller, mesh-catalog modules/mesh-delivery, mesh-catalog modules/gitea, mesh-catalog modules/build-agent] code: [mesh-delivery, mesh-catalog modules/mesh-delivery-gate, mesh-catalog modules/gitea, mesh-catalog modules/mesh-build-go, mesh-catalog modules/messenger, mesh-catalog modules/mesh-issues, mesh-controller, mesh-host, mesh-sdk]
updated: 2026-10-17 updated: 2026-10-26
decisions: decisions:
- "ADR 0239" - "ADR 0239"
- "ADR 0267" - "ADR 0267"
- "ADR 0276"
- "ADR 0282" - "ADR 0282"
- "ADR 0289" - "ADR 0289"
- "ADR 0304" - "ADR 0304"
@@ -17,74 +16,120 @@ decisions:
- "ADR 0382" - "ADR 0382"
- "ADR 0395" - "ADR 0395"
- "ADR 0398" - "ADR 0398"
- "ADR 0429"
- "ADR 0524"
- "ADR 0525"
- "ADR 0527"
- "ADR 0528"
- "ADR 0529"
- "ADR 0539"
- "ADR 0546"
- "ADR 0547"
--- ---
# Delivery — from a merge to the nodes, as it runs # Delivery — from a merge to the nodes, as it runs
What runs on the mesh's main branches on 2026-10-16, read from the code and the live record by research 055 What runs on 2026-10-26, after the delivery of release 6 (milestone R6 of
(findings 1 to 12). It is what [to-be 47](../01-to-be/47-delivery-from-commit-to-delivered.md) replaces, milestone [to-be 47](../01-to-be/47-delivery-from-commit-to-delivered.md), ADR 0539), read from the code on each repository's
by milestone. Until 2026-10-16 this document described the predecessor's pipeline, which no longer runs. main (`mesh-delivery`, the gate in `mesh-catalog`, the node agent in `mesh-host`, `mesh-controller`) and from the live
mesh: the controller's seat verbs, `mesh-controller.modules`, the gate's `table`, and the view's `assigner_wants` and
`node_haves`. Until 2026-10-26 this document described release 5, in which the controller still made a delivery plan
for each merge, ordered it in tiers and sent whole declarations to the machines; none of that runs any more.
## Who does what ## Who does what
- **mesh-delivery** owns deliveries and delivery groups (ADR 0239): one delivery per commit, a group per branch - **The parts of `mesh-delivery`** are modules of one repository, each a process with its own bus credential that
name shared across repositories. It records each transition in its state on the bus (key-value buckets watches keys and writes keys (ADR 0429). Running now: the forge bridge, the indexer, the checker, the teller and the
`deliveries` and `groups`), marks pull requests, asks the controller to deliver, and merges a pull request marked watchman on the control-node; the builder and the status view (`mesh-delivery-view`) on the workstation; and the
ready at its head once both required checks pass against the trunk's head (ADR 0364) and the operator approved assigner on the home server. The gate (`mesh-delivery-gate`, holding the seat `mesh-delivery`) is a module of
the merge into main on a channel that proves who answered (ADR 0398 §3). `mesh-catalog` and runs on the control-node.
- **The controller** plans, orders, sends and judges. It computes a delivery plan for a repository and the files a - **The forge bridge** turns the forge's events into stream messages and is the only part that calls the forge; the
merge changes, moves only modules whose build source changed with their build dependents (ADR 0267), and forge module (`gitea`) still announces them through the forge's system webhook.
reuses a merge check's build of the same reuse key (ADR 0350). - **The indexer** writes each module's fingerprint at the merge preview and at a trunk merge; **the builder** claims a
- **The builders** (holders of the build seat) take build requests and checks from a work-queue stream on the bus build once per fingerprint and orders it as an ask on the stream of the artifact's language.
and build inside a toolchain image another module provides, with the network off; on 2026-10-13 no node had Go - **The build modules** (`mesh-build-go` on all four machines, `mesh-build-node` on three, `mesh-build-image` on one;
or npm installed (issue 626). ADR 0525) hold the node seats `node-build-go`, `node-build-node` and `node-build-image` and pull asks from the bus's
- **The forge module** publishes every forge event on the bus through the forge's system webhook since 2026-10-14 work-queue streams (deliveries first, then checks, then re-checks) while their node has a free slot of build
(issue 729), and still polls the forge every 30 s, or every 300 s once it hears events. It keeps files of what it capacity.
announced so it does not announce twice. - **The checker** runs the merge check and writes the one status `mesh/check`, and serves a **try** on a pushed
branch. The forge requires no status on any repository (ADR 0534), and no repository holds a `merge-check.sh`
(ADR 0529).
- **The gate** keeps one key per pull request and the merge's states only: `proposed`, `ready`, `rejected`, `held`,
`merged` and `closed`, read from its `table` verb. On the operator's Approve it merges a delivery group's pull
requests in one pass and writes their merged heads as one revision of `source.main`. It sends nothing to a
machine and asks the controller nothing.
- **The controller** (`mesh-controller`, on the control-node) keeps registration, placement in its assignments
bucket and every node's inputs, and composes: on events it writes each module's signed composition for each node at
each fingerprint into the node's bucket `composed-<node>`, with its digest and its judgement of the move in
`mesh-controller_composed`, and it issues a module's credential and grant when its build is done. It plans,
orders and sends nothing: of its seat's 65 verbs, 20 are refused by name with their replacement, among them
`plans`, `plan`, `push`, `deliver`, `delivery-stop`, `delivery-order` and `delivery-walks` (read 2026-10-26 01:20).
- **The assigner** writes each node's want of each module, `want.<node>.<module>`, naming a fingerprint and the
digest of the one composition it allows, when its seven rules hold: 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` others ahead, every provider
runs healthy, the grant exists and the composition exists. In the code the canary is the first of the module's
nodes by name that is online (one ring), and rule 4 always holds, because the mesh keeps no `step`. It also runs
the composition check the checker reads. Its differences bucket, from release 5's comparison with what the
controller sent, is no longer written.
- **Two ways around the rules, both by hand.** A **put** (ADR 0546), written by the assigner's tool `assigner_put`,
puts one version of a module on one node: while a put owns a node's want of a module, from its ask until it ends,
the rules pass over that want. And the operator's credential may write a want itself, with a composition the
operator signed and wrote through mesh-sdk's `mesh-hand-compose` (ADR 0539 §10).
- **The node agent**, a process of the node-engine (`mesh-host`) on every node, watches its node's wants and its
bucket of compositions, and applies a module only when the want's fingerprint or digest differs from what runs,
and only the composition whose digest the want names, after checking its signature. It asks its node's
`node-backup` seat to back up a data module first and waits for the backup's end. It writes the have,
`have.<node>.<module>` in `node-engine_have`, with the fingerprint, the composition's digest, the phase and the
health. A failed apply is retried by the next change of the want or the composition; an apply that failed only
because the bus did not answer for a moment is retried with a bounded backoff, and at once when the bus is back.
- **The watchman** mirrors every want as a pulse owed by that node's agent until its have names the want's
fingerprint, so a node that does not move shows as a stall.
- **The status view** derives from `source.main`, the wants and the haves whether the source revision is
delivered, and says the event `delivered` once per revision, and `moving`, `settled` and `stuck` once for a
module on a node at one composition. The messenger holds the operator's notices on those events, and the issue
tracker hears `delivered`.
- **The grants part** (`mesh-grants`, on the control-node; ADR 0547) publishes each assignment's membership on the
assignments stream when it differs from the one last published. The bus's user list, assertions and grants it
composes only in its own state, compared with what the bus holds, and they take no effect; the controller still
composes the user list the bus loads.
## From a merge to a rollout ## From a merge to the nodes
- **The merge window.** The controller collects merges into an open batch; each merge restarts the window, which - A pull request's head is built and checked as it comes in; one status, `mesh/check`, carries the verdict, and a
closes after 90 seconds without a merge or 10 minutes after the batch's first merge (ADR 0276). A lone merge is delivery group has one verdict for every member. A move of the trunk asks the checks again, and a running check
cut at once (ADR 0282 §2, as ADR 0304 §1 changed it). The batch and its merges are rows in the controller's stops itself when its head or trunk commit moves.
store, cut by a timer every 5 s, with a catch-up timer every 5 minutes for merges no batch holds. - The gate merges on the operator's Approve and moves the source revision in one write. Nothing collects merges or
- **The rollout.** A closed batch becomes one rollout, at most one commit per repository, tiered along the module waits for others: the indexer writes the new fingerprints, the builder claims each new one once, the controller
graph. Rollouts run one at a time: a batch cut while a rollout is open is queued behind it, whatever it shares composes and grants on the done build, and the assigner writes each want once its rules hold, the canary first.
with it. Running unrelated rollouts side by side (ADR 0282 §3, ADR 0304 §2) is decided and not built (issue 525). A rollout's builds go to every machine at once, with no first-machine gate, and the rollout ends on - Each node applies its own wants one module at a time and writes its haves; the source revision is delivered when
the machines' reports or after fifteen minutes of silence (ADR 0360). A controller timer advances rollouts every every want equals its module's fingerprint and every have equals its want, running and healthy. Read on
30 s. 2026-10-26 at 01:20: 303 wants, every one equal to its node's have in fingerprint and composition digest, and 12
- **A merge during a rollout.** Its place in the next batch is a store row the open rollout's holder writes. On fingerprints held.
2026-10-16, when another controller worked the plans, a merge was not recorded and waited until a person released - **hq** builds no module: the checker runs hq's Go tests (`go test ./...`) as its check (ADR 0527).
it (issue 921).
- **"Release branches".** A pull request into a branch other than a trunk is judged by the controller built from that
branch (ADR 0358); feature 57 shipped about 37 packages that way over about 2.5 days and held the controller for
55.7 h.
## Checks
- Every pull request's head gets `mesh/merge-gate` (the composed machines) and `mesh/repo-check` (the repository's
`merge-check.sh`), each judged against one trunk commit (ADR 0323). A trunk move asks the check of every active,
cleanly merging pull request again (ADR 0395).
- A delivery group's members are each checked beside their siblings' heads, and the group also gets a composed
check, `mesh/delivery-group`; each member keeps its own gate beside it (ADR 0382).
- The queue's order (delivery builds first, then ready checks, then the rest) is kept by pausing the worker and
deleting and republishing a request (issue 558); an old head's check is ended after asking the forge for the
current head (issue 726).
- mesh-delivery ticks every 10 s, reconciles with the controller every 30 s and polls the forge for ready marks
every minute, although the forge module already publishes labeled, commented and synchronized events.
## Where the record of what runs lives ## Where the record of what runs lives
Builds and build requests, rollouts with their tiers, batches and merges, and assignments are rows in the Fingerprints, builds, verdicts, wants and haves are keys in the parts' buckets on the bus; compositions are in each
controller's store; artifacts are blobs in the artifact store by digest (ADR 0251); packages are versions in the node's `composed-<node>` bucket, readable only by that node, with a file over 64 KiB in the object store; builds'
package registry. The controller writes three record mirrors to the bus: assignments, declared config and files are in the artifact store by digest; packages are versions in the package registry. The controller
predefined keys; of the five ADR 0289 §7 decided, the nodes, rollouts, builds and settings mirrors are not in the still records every build it takes in, in its builds bucket (`RecordBuild` in `cmd/mesh-controller/key_takein.go`); its
code. An assignment names its module and node but not the build, artifact or rollout that put it there: which commit assignments bucket keeps placement alone, and its rollouts bucket keeps the record of releases 1 to 5 read-only, and
runs where, and which delivery brought it, takes three verbs and a join by hand. nothing writes it now.
No backup is taken before a delivery moves a module that holds data; backups run nightly and on demand (ADR 0214).
## What is not covered ## What is not covered
The integration tests run only in each repository's merge check; there are no end-to-end tests of delivery yet, Nothing checks the delivery budget: the over-budget condition and the gate's `times` went with release 6, and no
and the lab is not used (the operator, 2026-10-16). reader of the view's `settled` and `delivered` events measures it yet (issue 1945, note 10). The bus's own move stays
a person's step through the controller's `bus` verb (ADR 0539 §15). The checker's left-running keys of build modules
that stopped (`checker_status`'s `left_running`) are listed and hold no place. There are no end-to-end tests of
delivery, and the lab is not used (the operator, 2026-10-16).
## How it is checked
- `mesh-delivery-view.assigner_wants` and `mesh-delivery-view.node_haves`: each want beside its have; `indexer_modules`
and `builder_builds` for the fingerprints and builds they name.
- `mesh-delivery.table`: the gate's states and transitions; `mesh-delivery.deliveries` and `show` for one delivery.
- `mesh-controller.seats` and `mesh_overview`: the controller's verbs, and which are refused by name.
- The code: `mesh-delivery` (`modules/mesh-delivery-assigner/internal/assign`, `modules/mesh-delivery-view/internal/view`,
`modules/mesh-delivery-watchman/internal/watch`, `modules/mesh-grants`), `mesh-host` (`internal/agent`,
`cmd/mesh-host/nodeagent.go`), and the gate in `mesh-catalog` (`modules/mesh-delivery-gate`).
@@ -24,7 +24,7 @@ is what makes a node a participant — it is reachable whether or not anyone is
protocol. Its capability surface is larger, because it includes stand-ins for every capability protocol. Its capability surface is larger, because it includes stand-ins for every capability
discovered on peers. discovered on peers.
The two are the same code with the same catalogue. A capability is written once and is The two are the same code with the same catalog. A capability is written once and is
available to both. available to both.
## Anatomy naming, and what it obscures ## Anatomy naming, and what it obscures
@@ -52,7 +52,7 @@ For each assigned module the runtime, at startup:
4. For a service module, ensures it is installed and running. 4. For a service module, ensures it is installed and running.
Installation is idempotent and does the unglamorous work: ensure the runtime directory, Installation is idempotent and does the unglamorous work: ensure the runtime directory,
reconcile the link from the catalogue's definition into it, generate the environment, run any reconcile the link from the catalog's definition into it, generate the environment, run any
outstanding local migrations, create data directories with the right ownership, then start the outstanding local migrations, create data directories with the right ownership, then start the
service under supervision. service under supervision.
@@ -85,7 +85,7 @@ import is one-time and not persisted.
Three bootstrap scripts, and which one runs depends on the situation: Three bootstrap scripts, and which one runs depends on the situation:
- **First node.** Nothing exists yet, so the script stands up the database the rest of the - **First node.** Nothing exists yet, so the script stands up the database the rest of the
mesh reads from, publishes the catalogue, and starts the mesh. This resolves the mesh reads from, publishes the catalog, and starts the mesh. This resolves the
circularity of a mesh whose source of truth is itself a provisioned module. circularity of a mesh whose source of truth is itself a provisioned module.
- **Joining.** The node registers, takes its assignment from the database, and syncs. - **Joining.** The node registers, takes its assignment from the database, and syncs.
- **Rescue.** A node that cannot reach the mesh is brought back far enough to. - **Rescue.** A node that cannot reach the mesh is brought back far enough to.
+8 -8
View File
@@ -9,9 +9,9 @@ decisions:
- "ADR 0009" - "ADR 0009"
--- ---
# The catalogue, and what its shape says # The catalog, and what its shape says
The catalogue holds **124 modules**. Thirty-three belong to the mesh's own domain; the other The catalog holds **124 modules**. Thirty-three belong to the mesh's own domain; the other
ninety-one run *on* the mesh rather than being *of* it ninety-one run *on* the mesh rather than being *of* it
(ADR 0001). (ADR 0001).
@@ -30,7 +30,7 @@ By namespace, and the namespace records origin rather than purpose:
## What the flat level actually contains ## What the flat level actually contains
Grouped by what they are *for* — a grouping the catalogue itself does not express: Grouped by what they are *for* — a grouping the catalog itself does not express:
| Purpose | Roughly | | Purpose | Roughly |
|---|---| |---|---|
@@ -51,7 +51,7 @@ connectivity is made four times.
## What the shape records ## What the shape records
**The catalogue's shape records what was installed, not what anything is for.** One module is **The catalog's shape records what was installed, not what anything is for.** One module is
the unit of one piece of software, because that is the only granularity the module system the unit of one piece of software, because that is the only granularity the module system
offers. offers.
@@ -64,7 +64,7 @@ deliberately does not yet settle the domain list.
## Where the shape came from ## Where the shape came from
The catalogue's shape is not arbitrary and it is not a series of mistakes. **This repository The catalog's shape is not arbitrary and it is not a series of mistakes. **This repository
began as a dotfiles repository**, and most of what looks inexplicable is inherited from that began as a dotfiles repository**, and most of what looks inexplicable is inherited from that
directly. directly.
@@ -88,7 +88,7 @@ The generalisation from *place files on my machines* to *manage a mesh of nodes*
move and it worked. What it did not do is revisit the assumptions underneath, because they were move and it worked. What it did not do is revisit the assumptions underneath, because they were
never stated as assumptions — they were just how the thing already worked. never stated as assumptions — they were just how the thing already worked.
**This is the most useful single fact for anyone changing the catalogue**, and it is why the **This is the most useful single fact for anyone changing the catalog**, and it is why the
linking principle in particular reads as a deliberate architectural choice when it is an linking principle in particular reads as a deliberate architectural choice when it is an
inheritance. See ADR 0012, whose case inheritance. See ADR 0012, whose case
this strengthens: the argument for links was never made *for a mesh*. this strengthens: the argument for links was never made *for a mesh*.
@@ -109,10 +109,10 @@ module out of the repository safe.
**Placement is already decided.** A standalone application belongs in its own repository **Placement is already decided.** A standalone application belongs in its own repository
(ADR 0015), and reviewers reject (ADR 0015), and reviewers reject
it in the monorepo. The catalogue's flat level is not a dumping ground by policy; it is one by it in the monorepo. The catalog's flat level is not a dumping ground by policy; it is one by
history. history.
## Known inconsistencies in the catalogue itself ## Known inconsistencies in the catalog itself
Recorded because a reader will meet them: Recorded because a reader will meet them:
+2 -2
View File
@@ -11,7 +11,7 @@ decisions:
# The seats, as they run # The seats, as they run
Written from the controller's code and the catalogue's manifests on their main branches. This is Written from the controller's code and the catalog's manifests on their main branches. This is
the second piece of the new shape to exist, after [the lab](11-the-lab.md), and the first that the the second piece of the new shape to exist, after [the lab](11-the-lab.md), and the first that the
predecessor has no equivalent of: there, any well-formed name became a seat by being claimed. predecessor has no equivalent of: there, any well-formed name became a seat by being claimed.
@@ -24,7 +24,7 @@ have; nothing else can add to it.
Five seats deliver a provision: the mesh's store delivers the relational database, the mesh's broker Five seats deliver a provision: the mesh's store delivers the relational database, the mesh's broker
the message transport, the artifact-store seat the registry, and two more the package registry for the message transport, the artifact-store seat the registry, and two more the package registry for
one ecosystem and the git service. The remaining nine — the controller and catalogue seats, and the one ecosystem and the git service. The remaining nine — the controller and catalog seats, and the
node-scope ones for the builder, the DNS port, the packet filter, intrusion prevention, the node-scope ones for the builder, the DNS port, the packet filter, intrusion prevention, the
private network, the resolver's configuration and the showcase — mark a role without answering for private network, the resolver's configuration and the showcase — mark a role without answering for
anything a consumer requires. anything a consumer requires.
+22 -13
View File
@@ -2,8 +2,12 @@
layer: as-is layer: as-is
status: implemented status: implemented
code: [mesh-tools, mesh-host, mesh-controller] code: [mesh-tools, mesh-host, mesh-controller]
updated: 2026-10-17 updated: 2026-10-26
decisions: decisions:
- "ADR 0539"
- "ADR 0360"
- "ADR 0524"
- "ADR 0525"
- "ADR 0152" - "ADR 0152"
- "ADR 0175" - "ADR 0175"
- "ADR 0195" - "ADR 0195"
@@ -17,19 +21,22 @@ decisions:
**The `mesh-console` container no longer exists.** Read 2026-10-17 from `mesh-tools` (package **The `mesh-console` container no longer exists.** Read 2026-10-17 from `mesh-tools` (package
`node-tools`), `mesh-host` and `mesh-controller` on their forges' main, and from the live mesh `node-tools`), `mesh-host` and `mesh-controller` on their forges' main, and from the live mesh
through `mesh_overview`, `mesh_machine`, `mesh_runtimes` and `mesh_describe`. Checked: the through `mesh_overview`, `mesh_machine`, `mesh_runtimes` and `mesh_describe`. Checked: the
catalogue (`mesh-catalog`) carries no module named `mesh-console` any more — the name survives only catalog (`mesh-catalog`) carries no module named `mesh-console` any more — the name survives only
in the glossary's retired-words list and in a test fixture — and none of the four nodes answers a in the glossary's retired-words list and in a test fixture — and none of the four nodes answers a
module of that name. module of that name. How the tool runner reaches a node, its count of modules and tools and the
controller's verbs were read again on 2026-10-26, after the delivery of release 6 (ADR 0539).
## What it is now ## What it is now
**The tool runner is one of the mesh's core components**, named `node-tools` beside **The tool runner is one of the mesh's core components**, named `node-tools` beside
`mesh-controller`, `mesh-host` (the node-engine) and `nats` (the bus) — the mesh's own repositories' `mesh-controller`, `mesh-host` (the node-engine) and `nats` (the bus) — the mesh's own repositories'
check treats the four the same way, as the core whose health the mesh itself defines rather than a check treats the four the same way, as the core whose health the mesh itself defines rather than a
module's own. `mesh-host` and `mesh-controller` supervise, compose and account for it as a core module's own. Since release 6 it reaches each node the way every module does: the controller composes
process rather than as an ordinary assignment (`ComponentNodeTools`/`BusNodeTools` in both, the and signs its composition for that node, the assigner writes the node's want of it, and the
witness and lease code that restarts and rolls it back, the account named `<node>.node-tools` on node-engine's node agent applies it and writes its have. What stays particular to it is the account
the bus). named `<node>.node-tools` on the bus (`BusNodeTools` in `mesh-controller`) and the node-engine's
witness and lease code that puts back a tool runner that does not come up (`ComponentNodeTools` in
`mesh-host`, ADR 0360 §9).
**It still carries a manifest, `node-tools/module.json` in `mesh-tools`,** and that manifest is **It still carries a manifest, `node-tools/module.json` in `mesh-tools`,** and that manifest is
where its grants live: `invokes` names `*`, so its account may call every tool on the mesh; where its grants live: `invokes` names `*`, so its account may call every tool on the mesh;
@@ -58,7 +65,7 @@ body per request, no session and no event stream (`node-tools/internal/console/m
`mesh_machine`, `mesh_search`, `mesh_describe`, `mesh_call`, `mesh_runtimes` and `mesh_wait`. The `mesh_machine`, `mesh_search`, `mesh_describe`, `mesh_call`, `mesh_runtimes` and `mesh_wait`. The
handshake's `instructions` text names all seven and the four address forms. `mesh_overview` is handshake's `instructions` text names all seven and the four address forms. `mesh_overview` is
where the mesh-wide count of modules and tools is read live — a number copied into this document where the mesh-wide count of modules and tools is read live — a number copied into this document
would drift; asked on 2026-10-17 it answered 103 modules and 645 tools across the whole mesh, with would drift; asked on 2026-10-26 it answered 115 modules and 714 tools across the whole mesh, with
two module instances (`route-proxy` on two of the nodes) named as not answering rather than silently two module instances (`route-proxy` on two of the nodes) named as not answering rather than silently
absent. absent.
@@ -92,12 +99,14 @@ command, not inline, because one call blocks until it answers or times out.
**These already answer directly, not as a stated future.** Beside every assigned module's tools, **These already answer directly, not as a stated future.** Beside every assigned module's tools,
the tool runner asks the `mesh-controller` seat for its own verbs the same way (ADR 0154): measured the tool runner asks the `mesh-controller` seat for its own verbs the same way (ADR 0154): measured
live, the mesh-controller runtime is one instance, mesh-wide, and it answers 63 verbs for that one live on 2026-10-26, the mesh-controller runtime is one instance, mesh-wide, and it answers 65 verbs for
seat (`status`, `push`, `assign`, `build`, `artifacts` and the rest), and the `mesh-controller` that one seat (`status`, `assign`, `modules`, `artifacts` and the rest), of which 20 are refused by
name with what replaces them: the delivery verbs release 6 removed (`plans`, `plan`, `push`,
`deliver` among them) and the build verbs of release 4 (`build`, `queue` among them). The `mesh-controller`
*module* answers none of its own — it serves no module tools, only its seat's, exactly as design 33 *module* answers none of its own — it serves no module tools, only its seat's, exactly as design 33
described while it was still only designed. The 56-to-93 seat-verb counts each node's tool runner described while it was still only designed. The 56-to-93 seat-verb counts each node's tool runner
answers (below) are a different thing: the tool runner serves the verbs of whichever node-scoped answers (below) are a different thing: the tool runner serves the verbs of whichever node-scoped
seats are held on that node (`node-backup`, `node-build-agent`, `node-service-manager` among them), seats are held on that node (`node-backup`, `node-build-go`, `node-service-manager` among them),
on behalf of the modules holding them, not the mesh-controller seat's — that one is served by the on behalf of the modules holding them, not the mesh-controller seat's — that one is served by the
mesh-controller runtime itself, a separate process, and the tool runner only asks it and relays what mesh-controller runtime itself, a separate process, and the tool runner only asks it and relays what
it answers. it answers.
@@ -111,11 +120,11 @@ to-be document still describes it as assigned to "whichever machines an operator
actually runs is the opposite — every node the mesh holds carries one, because it is core actually runs is the opposite — every node the mesh holds carries one, because it is core
infrastructure the mesh itself depends on (discovery, the controller's own verbs, every module's infrastructure the mesh itself depends on (discovery, the controller's own verbs, every module's
tools), not a convenience assigned to a workstation. There is accordingly no assign or unassign verb tools), not a convenience assigned to a workstation. There is accordingly no assign or unassign verb
for it: removing it from a node is not an operation the controller or the catalogue exposes. for it: removing it from a node is not an operation the controller or the catalog exposes.
## What this repository's prior text got wrong ## What this repository's prior text got wrong
The document this replaces described `mesh-console` as a catalogue module served by a container and The document this replaces described `mesh-console` as a catalog module served by a container and
assigned only to workstations. That module and its container are gone; the manifest is not — it assigned only to workstations. That module and its container are gone; the manifest is not — it
moved to `node-tools/module.json`, still declares `invokes: ["*"]`, and gained `waits` and the moved to `node-tools/module.json`, still declares `invokes: ["*"]`, and gained `waits` and the
`mcp-endpoint` provision, described above. What also moved is which language builds it: the `mcp-endpoint` provision, described above. What also moved is which language builds it: the
+3 -2
View File
@@ -5,6 +5,7 @@ code: [mesh-controller internal/catalogue/state.go, mesh-controller internal/bro
updated: 2026-10-04 updated: 2026-10-04
decisions: decisions:
- "ADR 0201" - "ADR 0201"
- "ADR 0439"
--- ---
# A module's state, as it runs # A module's state, as it runs
@@ -17,7 +18,7 @@ machine and its licence manager; on the day this was written, three buckets exis
## What runs ## What runs
- **The controller** creates a key-value bucket `<module>_<name>` for every declared state, from the - **The controller** creates a key-value bucket `<module>_<name>` for every declared state, from the
catalogue — on every start and, since the first module that declared state found it missing, on every catalog — on every start and, since the first module that declared state found it missing, on every
push before the memberships that name it. A bucket nothing declares any more is reported and kept. push before the memberships that name it. A bucket nothing declares any more is reported and kept.
Every bucket carries the mesh's caps: 256 KiB a value, 64 MiB a bucket. Every bucket carries the mesh's caps: 256 KiB a value, 64 MiB a bucket.
- **The grants**: the machine's runtime is granted, for each bucket a module it carries owns, writing - **The grants**: the machine's runtime is granted, for each bucket a module it carries owns, writing
@@ -45,7 +46,7 @@ machine and its licence manager; on the day this was written, three buckets exis
## How it is checked ## How it is checked
The controller's catalogue and broker tests (names, grants, memberships, a bucket asserted in place The controller's catalog and broker tests (names, grants, memberships, a bucket asserted in place
against a real server); the runtime's tests over a real bus (current values without deletions, refusals, against a real server); the runtime's tests over a real bus (current values without deletions, refusals,
the TypeScript SDK through the runtime); `module check` names a read whose owner on the shelf keeps no the TypeScript SDK through the runtime); `module check` names a read whose owner on the shelf keeps no
such state. such state.
+13 -1
View File
@@ -2,9 +2,11 @@
layer: as-is layer: as-is
status: implemented status: implemented
code: [mesh-controller, mesh-tools] code: [mesh-controller, mesh-tools]
updated: 2026-10-16 updated: 2026-10-19
decisions: decisions:
- "ADR 0106" - "ADR 0106"
- "ADR 0436"
- "ADR 0438"
--- ---
# Logins on the bus, as they run # Logins on the bus, as they run
@@ -25,6 +27,16 @@ Nothing marks what a tool does.** Read from the controller's and the tool runner
- **The caller is in the address.** A call's address carries the caller's name, so the runtime takes the caller - **The caller is in the address.** A call's address carries the caller's name, so the runtime takes the caller
from the address, never from the message. For a seat every machine holds, the address also names the machine. from the address, never from the message. For a seat every machine holds, the address also names the machine.
- **A module's process may have a login of its own** (ADR 0436, delivered 2026-10-19 in mesh-controller #390 and
mesh-sdk #36). A `process` resource that declares `bus` runs as a user its module makes and gets one login per
machine, named by machine, module and process. It may emit its declared events, take work from its module's own
work-queue stream through the one consumer the controller makes for it, use its module's states, read the
router's records of its module's asks, call the verbs it declares, and listen only on its own inbox. It can
answer no call and hold no seat. `module issue` delivers its password as the module's own secret
`broker-<process id>`, readable by that user alone. mesh-mail's sender is the first such process: it holds the
mail passwords that the tool runner may not read, and sends each mail the operator approved, which the tools
record as queued before they put it on its work queue (ADR 0438).
## What it cannot do ## What it cannot do
- **No expiry.** A login lives until its record is deleted and the list rewritten on the bus's node. - **No expiry.** A login lives until its record is deleted and the list rewritten on the bus's node.
@@ -15,7 +15,7 @@ decisions:
**A node record names at most two logins: the operator account, always, and an agent account, on one node of **A node record names at most two logins: the operator account, always, and an agent account, on one node of
four. Where a node names no agent account, agents run as the operator account. Nothing ties an identity to a four. Where a node names no agent account, agents run as the operator account. Nothing ties an identity to a
login, and a module that needs one declares it itself.** Read on 2026-10-17 from the controller's and the login, and a module that needs one declares it itself.** Read on 2026-10-17 from the controller's and the
catalogue's code on main (both local clones were a few commits behind the forge, so each claim below was catalog's code on main (both local clones were a few commits behind the forge, so each claim below was
also checked against a live read) and from the live node records. The change planned in ADR 0411 to 0414 also checked against a live read) and from the live node records. The change planned in ADR 0411 to 0414
replaces this document when it is built. replaces this document when it is built.
@@ -113,4 +113,4 @@ Not here: see [`16-logins-on-the-bus.md`](16-logins-on-the-bus.md).
tools for its status. tools for its status.
- `knowledge-base.get` with `kind` decision and number 266, 409 or 410; `issue-tracker.get` with 989 or 991. - `knowledge-base.get` with `kind` decision and number 266, 409 or 410; `issue-tracker.get` with 989 or 991.
- The code: the controller's node-record and agent-account files, and the `claude-code` module's manifest and - The code: the controller's node-record and agent-account files, and the `claude-code` module's manifest and
guard in the catalogue. Their unit tests hold the node record's refusals and the guard's account rule. guard in the catalog. Their unit tests hold the node record's refusals and the guard's account rule.
+286
View File
@@ -0,0 +1,286 @@
---
layer: as-is
status: implemented
code: [mesh-catalog modules/mesh-mail, mesh-catalog modules/mesh-mail-web, mesh-catalog modules/mesh-mail-notifier, mesh-catalog modules/mesh-mail-reader]
updated: 2026-10-22
decisions:
- "ADR 0359"
- "ADR 0361"
- "ADR 0378"
- "ADR 0379"
- "ADR 0436"
- "ADR 0439"
- "ADR 0440"
- "ADR 0459"
- "ADR 0461"
- "ADR 0472"
---
# 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 fourth, `mesh-mail-reader`, holds the seat
`mail-reading`: it hears every new mail, reads it in small batches, and keeps a mail entry — its mail group, a
one-line summary, its mark and why, never a body — for the mail page to show; the operator's own corrections
on the mail page become reading lessons that outrank a mail's own words in a later batch. 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; `mesh-mail-reader` is on that list
too, and asks no send, by its own design rather than by a narrower grant. 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 `835bf22b` (2026-10-22) and from the live mesh through the mesh MCP
server on 2026-10-22.
```
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-reader on it too, asking no send)
│
┌────┴────────────────┬─────────────────────────┬──────────────────────────────────┐
mesh-mail-notifier mesh-mail-reader mesh-mail-web (mail-webclient,
(mail-notifier, once (mail-reading, once for once for the mesh — the control-node)
per machine — laptop, the mesh — the control- sign-in (Keycloak) ─ pages ─ send ask ─▶ operator's Approve
workstation) node) ▲ (Telegram)
node-notifier.send on hears MAIL's added events; │ entries groups correct
new inbox mail batches of 10 judged ─▶ its own │ lessons undo status
database (entries, groups, lessons) ─▶ ─┘
mail signal ─▶ operator's channel (Telegram)
```
## 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.
- **`mesh-mail-reader`** is a module of its own (`modules/mesh-mail-reader`), with its own Go module and
source (`go.mod`, `cmd/mesh-mail-reader`), importing nothing of `mesh-mail` (`callers.go`). It claims `mail-reading`, a fourth seat `mesh-mail`'s own manifest declares beside its three mail
seats, held once for the mesh (ADR 0459 §1, §2). It is assigned to the same machine as `mesh-mail`, under
its own account, `mesh-mail-reader` (`runs-as`; confirmed live, 2026-10-22). It is covered whole in §8.
- None of the four seats is named `mesh-*`. A caller reaches them by seat (`mail-store.<verb>`,
`mail-webclient.<verb>`, `mail-notifier.<verb>`, `mail-reading.<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-22): `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; the control-node's `mesh-mail-web`; and
the control-node's `mesh-mail-reader`. So the notifier, every machine's agent sessions, the web client and
the mail reader can each both read an account's mail and ask to send from it, from the one list — ADR 0472
§1 and §3 decided the mail reader joins this same list, rather than a narrower one of its own, and that it
simply never calls a send verb: its manifest invokes `seat:mail-store.read` and `seat:mail-store.accounts`
alone, no send verb of `mail-store` (`modules/mesh-mail-reader/module.json`, confirmed on `origin/main` at
`835bf22b`).
## 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`), a mail correction (`/mail-correct`) and a reading-lesson undo (`/mail-undo`) — plus
the sign-in callback (`/signed-in`). Every changing form (the send, a correction, an undo) carries a token
bound to the session (`server.go`, `csrf.go`).
- **The accounts page now opens on mail reading, above the classical list** (ADR 0459 §13, issue
1430, 1523; `accounts.go`). It asks `mail-reading.status` first; not answering at all makes the whole
section say mail reading is unavailable and why, and nothing else here is asked — the classical list below
still renders regardless. Otherwise it shows, in order: the mail that matters (`mail-reading.entries`, mark
`matters`), each with a correction form (mark, mail group) that posts to `/mail-correct`; the mail groups
(`mail-reading.groups`) when mail reading is healthy, or the unhealthy cause and since when in their place
when it is not; the scam warnings (the same `entries` answer, mark `scam warning`), each with the same kind
of correction form, shown either way; the reading lessons (`mail-reading.lessons`, newest first), each with
its own undo form posting to `/mail-undo`; and the weekly reading-lessons-per-100-read-mails measure, from
`mail-reading.status`'s own answer. A mail body never reaches this section — only a mail entry's headers,
mail group, summary, mark, why and due date.
- **A correction or an undo is asked of `mail-reading`, not acted on here.** `/mail-correct` and `/mail-undo`
are refused with no sign-in or a form token from another session, before `mail-reading.correct` or
`.undo` is ever asked; whatever `mail-reading` answers or refuses, the page is simply shown again, so a
refused correction is never surfaced as its own error (`mailwrite.go`, issue 1523).
- **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-22). 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 keeps no database of its own.** 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). Its manifest now also invokes `mail-reading`'s six verbs (ADR 0459 §13): it
reads `entries`, `groups`, `lessons` and `status`, and writes a correction or an undo with `correct` and
`undo` — writes that land in `mail-reading`'s own database, never in anything `mesh-mail-web` keeps itself.
## 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-22; `mesh_machine`
for each shows its seat `mail-notifier` and tool `mail_notifier_health` there, and neither on the
home server or the control-node), where it needs `node-notifier` to be held beneath it. ADR 0461 §7 names
these same two machines as the ones it is to be unassigned from, on the operator's Approve, once mail
signals (§8) are proven live on the operator's real accounts (issue 1418) — as built, that unassignment has
not happened.
## 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`).
## 8. `mesh-mail-reader`
- **It claims `mail-reading`**, the fourth mail seat, held once for the mesh, serving six verbs:
`entries`, `groups`, `correct`, `lessons`, `undo` and `status` (ADR 0459 §1, §2; `module.json`,
`tools.go`). Each answers only a caller the trusted setting `callers` names — a bare module name, granted
on any machine, `<machine>.<module>`, or `person.<name>`, the same shapes `mesh-mail`'s own `grants`
matches, copied in miniature so this module never imports `mesh-mail`'s code (`callers.go`). The setting
defaults to `["mesh-mail-web"]` and is unchanged on the live mesh (`mesh-controller.settings` for
`mesh-mail-reader`, read live on 2026-10-22), so only `mesh-mail-web` may call a `mail-reading` verb today;
any other caller is refused, naming why.
- **Its own account and database.** It runs as its own account, `mesh-mail-reader` (`runs-as`), never the
tool runner's, because it hears `mesh-mail`'s restricted stream `MAIL` under it. It is granted reading on
every account `mesh-mail` holds — named as the control-node's `mesh-mail-reader` in `mesh-mail`'s `grants`
for each of the three accounts alike (`mesh-controller.settings` for `mesh-mail`, read live on 2026-10-22) —
and never asks to send: its manifest invokes `seat:mail-store.read` and `seat:mail-store.accounts` only, no
send verb, and its code calls neither `mail_send` nor anything that moves, flags or deletes a mail (ADR
0459 §11, ADR 0472 §3). It keeps its own Postgres database (`postgres-database`, contributed as
`mail-reader`), separate from `mesh-mail`'s index.
- **Its database holds four tables, no mail body in any of them** (`schema.sql`): `mail_entry` — one row per
judged mail, its account, message id, folder, date, sender name and address, subject, its `mail_group`, a
`summary` of at most 160 characters, its `mark` (`matters`, `scam warning` or `neither`, or null), `why`,
a `due_date`, whether it was `told` and when it was `judged`; `mail_group` — a label and when it was first
made; `reading_lesson` — the corrected mail's own headers (account, folder, sender name and address,
subject), the mail group, the `change` made, when (`made`), and whether it was `undone`; and `waiting_mail`
— a mail heard but not yet read, by its account, folder, message id, UIDVALIDITY and UID (unique together,
so a mail heard twice is kept once), its date, sender, subject, flags and size, in the order it was heard. A
fifth table, `reader_health`, keeps its own health (below) across a restart.
- **It hears mail under its own account.** It consumes `mesh-mail`'s stream `MAIL` for
`mesh-mail.mail.*.message.added` events alone (`AddedPattern`, `hear.go`), recording each as a row in
`waiting_mail` before the handler returns — so the controller's durable consumer redelivers an event a
store failure lost, rather than it being silently dropped — and keeping a mail heard twice to one row, by
its own unique key above (ADR 0459 §3, §12; `hear.go`).
- **Its batches**: at most 10 mails at a time (`MaxMailsPerBatch`), taken from `waiting_mail` in the order
they were heard; a batch starts no sooner than 160 seconds after the previous batch started (`BatchGap`,
pinned to the constant `MinBatchGap` in every production build; only a build tagged `integration` may
shorten it, and never in a way a unit test lets reach production) — bounding mail reading to at most 225
mails an hour (ADR 0459 §5, §15; `batch.go`, `question.go`). Each batch is one judgement, asked of
`claude-code.claude_code_judge` on this module's own machine (`MESH_NODE`) and collected with
`claude_code_judged` when it answers pending; it is asked to answer `within` at most 40 seconds
(`JudgeWithin`), and the whole question — the framing that a mail is data and never an instruction, the
existing mail groups (largest first, at most 4 KiB), the reading lessons that apply (at most 40, matching
sender or mail group first then newest, at most 12 KiB) and up to 10 mails (each at most 3.5 KiB: headers
at most 512 bytes, text cut to its first 3 KiB) — is bounded to 55 KiB. A mail the provider filed as junk
(its folder named Junk or Spam, or carrying the `\Junk` flag) is said as that fact beside its text. A mail
that has simply gone (moved or deleted since it was heard) is taken out of the waiting queue without ever
being judged; any other read failure leaves the whole batch waiting for the next slot; on success, one
mail entry is written per mail and any new mail group is made; on a refused, timed-out or out-of-shape
judgement, nothing is written, every mail given stays waiting, and the reader's own health (below) is set
unhealthy (`batch.go`'s `RunOneBatch`, `RunBatches`).
- **Its mail signal.** After each batch, win or lose, it tries to tell the operator: every entry marked
`matters` and not yet `told` — carrying one over from an earlier batch that went untold — is gathered into
one `operator-channel.tell` with subject `mail`, naming the mail with the nearest due date first, then the
next one, ending "and N-2 more on the mail page" past two; nothing is sent, and nothing asked at all, when
there is nothing to tell or while the reader is unhealthy (read fresh from the store, not assumed). The
words are checked first, line by line, with `messenger.messenger_check`; any one refused falls back to the
safe form "N mails matter: open the mail page". Only once `tell` answers that some channel actually sent it
are those entries marked `told` (ADR 0461 §1-§6; `tell.go`, `signal.go`).
- **Its health**, `mail_reader_health`, is asked by the node-engine itself (`runtime-health` of kind `tool`,
every 60 seconds) and carries no caller check. It is unhealthy once a batch judgement is refused, times
out, or answers out of shape, naming which of the three, the judgement's own words, and since the first
such failure in a row (a later one in the same run never pushes that time forward); healthy again once a
judgement succeeds. The state survives a restart, kept in its own database rather than only in memory (ADR
0459 §12; `health.go`).
- **What it never does.** It changes no mail: it moves, flags, deletes and sends nothing, asking no verb of
`mail-store` that would (ADR 0459 §11). It keeps no mail body and no text taken from one — only headers, a
summary at most 160 characters, a mark, why, and a due date (ADR 0459 §4).
+2 -1
View File
@@ -18,7 +18,7 @@ Where the two disagree, the implementation wins and the disagreement is stated.
| [`07-knowledge.md`](07-knowledge.md) | The mesh keeps no store: what it knows is what modules answer, and the record is read by one | | [`07-knowledge.md`](07-knowledge.md) | The mesh keeps no store: what it knows is what modules answer, and the record is read by one |
| [`08-agents-and-work.md`](08-agents-and-work.md) | Agents as employees, tasks, workflows, and the meeting model | | [`08-agents-and-work.md`](08-agents-and-work.md) | Agents as employees, tasks, workflows, and the meeting model |
| [`09-interfaces-and-observability.md`](09-interfaces-and-observability.md) | How the mesh is reached and watched — tools, board, proxy, health, thoughts | | [`09-interfaces-and-observability.md`](09-interfaces-and-observability.md) | How the mesh is reached and watched — tools, board, proxy, health, thoughts |
| [`10-module-catalogue.md`](10-module-catalogue.md) | The catalogue's shape, and what its shape says | | [`10-module-catalogue.md`](10-module-catalogue.md) | The catalog's shape, and what its shape says |
| [`11-the-lab.md`](11-the-lab.md) | The lab — the first piece of the new shape that exists, and what it does not yet do | | [`11-the-lab.md`](11-the-lab.md) | The lab — the first piece of the new shape that exists, and what it does not yet do |
| [`12-the-seats.md`](12-the-seats.md) | The seats the mesh defines, who holds one, and where a seat changes resolution | | [`12-the-seats.md`](12-the-seats.md) | The seats the mesh defines, who holds one, and where a seat changes resolution |
| [`13-the-console.md`](13-the-console.md) | The mesh's tools on the machine a person sits at, served by a module the mesh assigned there | | [`13-the-console.md`](13-the-console.md) | The mesh's tools on the machine a person sits at, served by a module the mesh assigned there |
@@ -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 | | [`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 | | [`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 | | [`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 ## What these documents are not
+8 -8
View File
@@ -431,17 +431,17 @@ It is the same rule the node-engine follows about a service that does not exist
(ADR 0004) — absence must be distinguishable (ADR 0004) — absence must be distinguishable
from a failure to answer — applied to coverage instead of to a machine. from a failure to answer — applied to coverage instead of to a machine.
## A bed reads the catalogue it proves ## A bed reads the catalog it proves
*Written 2026-09-21, from resolving [04-ISSUES/073](../../04-ISSUES/073-beds-carry-copies-of-catalogue-manifests/00-report.md); *Written 2026-09-21, from resolving [04-ISSUES/073](../../04-ISSUES/073-beds-carry-copies-of-catalogue-manifests/00-report.md);
decided in ADR 0089.* decided in ADR 0089.*
The rule above — the run rebuilds what it tests — was held for binaries and images and not for The rule above — the run rebuilds what it tests — was held for binaries and images and not for
manifests. Beds built the manifests they install inline, as literals copied from the catalogue manifests. Beds built the manifests they install inline, as literals copied from the catalog
when each bed was written; the copies did not move when the catalogue did, and a catalogue change when each bed was written; the copies did not move when the catalog did, and a catalog change
was proven by no bed at all, while every bed stayed green against its copy. was proven by no bed at all, while every bed stayed green against its copy.
**A bed that installs a catalogue module reads that module's manifest from the catalogue the run **A bed that installs a catalog module reads that module's manifest from the catalog the run
was pointed at.** It rewrites what the lab must — a build artifact becomes the image the machine was pointed at.** It rewrites what the lab must — a build artifact becomes the image the machine
holds, an image is pinned, a host port is remapped where one machine carries colliding modules, holds, an image is pinned, a host port is remapped where one machine carries colliding modules,
an address may point at a stand-in the bed raises — and nothing else. an address may point at a stand-in the bed raises — and nothing else.
@@ -451,15 +451,15 @@ server, a secret in the environment, a requirement edge cut so no second provide
that is a mesh test. It does not get a name of its own if it runs the module's runtime — a that is a mesh test. It does not get a name of its own if it runs the module's runtime — a
module's name is its tool namespace and its broker scope module's name is its tool namespace and its broker scope
(ADR 0093) — (ADR 0093) —
so it reads the catalogue and installs what the module requires; a name of its own is for a so it reads the catalog and installs what the module requires; a name of its own is for a
fixture that runs no real runtime. fixture that runs no real runtime.
**The receipt names the catalogue's commit** with the others', so a run taken before a manifest **The receipt names the catalog's commit** with the others', so a run taken before a manifest
changed says so — the same rule as for the binaries, for the same reason. changed says so — the same rule as for the binaries, for the same reason.
*How it is checked:* a unit test in the lab refuses an inline manifest literal that names a *How it is checked:* a unit test in the lab refuses an inline manifest literal that names a
catalogue module unless the bed is declared, with its reason, in the test's own list, and refuses catalog module unless the bed is declared, with its reason, in the test's own list, and refuses
a declaration for a copy that is gone; the receipt test asserts the catalogue is claimed whenever a declaration for a copy that is gone; the receipt test asserts the catalog is claimed whenever
the run is pointed at one. the run is pointed at one.
## Consequences ## Consequences
+1 -1
View File
@@ -31,7 +31,7 @@ offered one driver, everything worked, and snapshots took **seventy-six times lo
needed to. Nothing failed. Nothing warned. A lab in that state runs correctly and is simply too needed to. Nothing failed. Nothing warned. A lab in that state runs correctly and is simply too
slow to use — and the inner loop it exists to provide quietly does not exist. slow to use — and the inner loop it exists to provide quietly does not exist.
The second is the more dangerous shape and it is a variant the mesh has not catalogued before. The second is the more dangerous shape and it is a variant the mesh has not cataloged before.
Its usual failure is *reported success and did nothing*. This is **reported success and did it Its usual failure is *reported success and did nothing*. This is **reported success and did it
seventy-six times slower**, which no error surface catches because nothing is wrong. seventy-six times slower**, which no error surface catches because nothing is wrong.
+1 -1
View File
@@ -177,7 +177,7 @@ raises the bootstrap forge under the forge module's container name, with the mod
its data directory, so the module holds it by the found rule; the network is the one difference a take its data directory, so the module holds it by the found rule; the network is the one difference a take
has left to say. *How it is checked:* a host test joins a kept network and refuses one it cannot; a has left to say. *How it is checked:* a host test joins a kept network and refuses one it cannot; a
host test keeps a left-out module's record and hold and removes an absent module's; a bootstrap test host test keeps a left-out module's record and hold and removes an absent module's; a bootstrap test
holds the installer's constants to the module's manifest where the catalogue is checked out beside it. holds the installer's constants to the module's manifest where the catalog is checked out beside it.
**What filters the machine, and the found firewall kept retired** — revision, 2026-10-02 **What filters the machine, and the found firewall kept retired** — revision, 2026-10-02
(ADR 0168). The node-engine (ADR 0168). The node-engine
+1 -1
View File
@@ -326,7 +326,7 @@ shape moves — it names a thing and the node-engine fetches it — and the cont
genesis must raise before the controller can exist. It still raises one, for the store and the broker, genesis must raise before the controller can exist. It still raises one, for the store and the broker,
which is where somebody else's software belongs. which is where somebody else's software belongs.
The mesh's own components — the node-engine, the controller, the catalogue, the builder, the vault — are The mesh's own components — the node-engine, the controller, the catalog, the builder, the vault — are
delivered as binaries into directories named for their versions, by the mechanism delivered as binaries into directories named for their versions, by the mechanism
ADR 0141 describes. Third-party ADR 0141 describes. Third-party
software stays a container. The split is not about isolation; it is about who built the thing. software stays a container. The split is not about isolation; it is about who built the thing.
+5 -5
View File
@@ -425,8 +425,8 @@ time.*
A network manager rewrites `/etc/resolv.conf` on every connectivity change unless it is told not to A network manager rewrites `/etc/resolv.conf` on every connectivity change unless it is told not to
(ADR 0117), so the module holding (ADR 0117), so the module holding
`node-uplink` — the one telling it — writes the file itself, and there is one owner for it, the one `node-uplink` — the one telling it — writes the file itself, and there is one owner for it, the one
whose program would otherwise overwrite it. Each of the catalogue's managers' modules whose program would otherwise overwrite it. Each of the catalog's managers' modules
(NetworkManager, systemd-networkd; dhcpcd's left the catalogue with (NetworkManager, systemd-networkd; dhcpcd's left the catalog with
ADR 0226, held by no machine) renders the same template from the resolver's holders and ADR 0226, held by no machine) renders the same template from the resolver's holders and
requires the mesh's resolver, so a machine is refused when nothing in the mesh resolves rather than requires the mesh's resolver, so a machine is refused when nothing in the mesh resolves rather than
given a file listing nothing. The managers' own mechanisms were weighed and not used: NetworkManager's given a file listing nothing. The managers' own mechanisms were weighed and not used: NetworkManager's
@@ -440,7 +440,7 @@ node declaring one path are refused. The module that wrote the file before, its
retire. The file changes owner in one apply on each machine: the node-engine hands a whole file to the retire. The file changes owner in one apply on each machine: the node-engine hands a whole file to the
resource declaring its path now rather than removing it first, so a machine is never without it. resource declaring its path now rather than removing it first, so a machine is never without it.
*Checked by the controller's tests that the uplink modules carry one identical template and that *Checked by the controller's tests that the uplink modules carry one identical template and that
nothing else in the catalogue writes the path, a resolution test refusing a second writer, and the nothing else in the catalog writes the path, a resolution test refusing a second writer, and the
node-engine's handover test, in which the file is present at every step of the apply.* node-engine's handover test, in which the file is present at every step of the apply.*
**A machine's names are one seat's** (ADR 0223). **A machine's names are one seat's** (ADR 0223).
@@ -643,7 +643,7 @@ to stop working.*
### The name is a label, not a domain ### The name is a label, not a domain
*2026-09-09, from running the whole mesh in the lab.* A route contribution carried its public name *2026-09-09, from running the whole mesh in the lab.* A route contribution carried its public name
in full — the forge as `git.example.tld`, spelt out in the module. Pointing the same catalogue at a in full — the forge as `git.example.tld`, spelt out in the module. Pointing the same catalog at a
different domain — a lab standing in for production, or a second operator's mesh — meant rewriting different domain — a lab standing in for production, or a second operator's mesh — meant rewriting
that name on every routed module. The mesh was holding a **map of names to services**, which is the that name on every routed module. The mesh was holding a **map of names to services**, which is the
one thing it must not: a public name is two facts owned by two different places, and neither is the one thing it must not: a public name is two facts owned by two different places, and neither is the
@@ -976,7 +976,7 @@ run-once step before it starts. The step names the binding it reads and the prox
step, so when the authority moves the root is fetched again and the proxy is recreated with it step, so when the authority moves the root is fetched again and the proxy is recreated with it
(ADR 0099). (ADR 0099).
*How it is checked:* the route-forwarding bed installs the authority, the proxy and a consumer *How it is checked:* the route-forwarding bed installs the authority, the proxy and a consumer
from the catalogue and asserts the routed name is served. from the catalog and asserts the routed name is served.
**And a machine trusts that authority because a module put its root in its trust store** **And a machine trusts that authority because a module put its root in its trust store**
(ADR 0147). The proxy's fetch (ADR 0147). The proxy's fetch
+10 -10
View File
@@ -57,10 +57,10 @@ produced from there. A module whose path is the repository root is the ordinary
not a separate shape. not a separate shape.
*Corrected 2026-09-12. This section previously said the manifest was at the root, full stop, which *Corrected 2026-09-12. This section previously said the manifest was at the root, full stop, which
made a repository hold exactly one module. Nothing that exists is shaped that way: the catalogue made a repository hold exactly one module. Nothing that exists is shaped that way: the catalog
holds sixty-seven modules in their own directories, the system being replaced has always built a holds sixty-seven modules in their own directories, the system being replaced has always built a
module from a repository and a path, and the root-only reading left every existing module module from a repository and a path, and the root-only reading left every existing module
unbuildable — pointed at the catalogue the builder finds no manifest, pointed at a module's source unbuildable — pointed at the catalog the builder finds no manifest, pointed at a module's source
it finds no manifest either.* it finds no manifest either.*
## A manifest is checked where it is written ## A manifest is checked where it is written
@@ -84,7 +84,7 @@ read without a store (ADR 0122
keeps the store authoritative, so a claim on a mesh seat is judged fully only at registration, and the keeps the store authoritative, so a claim on a mesh seat is judged fully only at registration, and the
check says that too). check says that too).
*How it is checked:* the controller's test runs the check over the catalogue checkout beside it and *How it is checked:* the controller's test runs the check over the catalog checkout beside it and
over a manifest with a known fault, and asserts the first passes and the second names the fault; the over a manifest with a known fault, and asserts the first passes and the second names the fault; the
test that used to be the only check, `TestEveryCatalogueManifestParses`, now stands beside a command test that used to be the only check, `TestEveryCatalogueManifestParses`, now stands beside a command
anybody can run. anybody can run.
@@ -218,7 +218,7 @@ for, arrived at by an implementation detail.
until it is told otherwise; the mesh changing its mind is not a machine acting on it, and until it is told otherwise; the mesh changing its mind is not a machine acting on it, and
collapsing the two is how a mesh comes to report success for something that has not happened. collapsing the two is how a mesh comes to report success for something that has not happened.
*Checked end to end: a commit, a build, a catalogue entry, and a machine that ends up running what *Checked end to end: a commit, a build, a catalog entry, and a machine that ends up running what
the source says — with both halves that make the answer trustworthy. It is still running the old the source says — with both halves that make the answer trustworthy. It is still running the old
one until it is pushed, and it stops being reported as behind once it has caught up, because a one until it is pushed, and it stops being reported as behind once it has caught up, because a
status that says "behind" for ever is one nobody reads.* status that says "behind" for ever is one nobody reads.*
@@ -398,7 +398,7 @@ that distinction once already.*
## The four the loop cannot build ## The four the loop cannot build
*2026-09-12. Written down because it keeps being rediscovered as if it were new, once per *2026-09-12. Written down because it keeps being rediscovered as if it were new, once per
component. It is one rule; the instances are listed below. Revised the same day, when the catalogue component. It is one rule; the instances are listed below. Revised the same day, when the catalog
gained an owner and turned out to be a fourth — which is the argument for stating the rule rather gained an owner and turned out to be a fourth — which is the argument for stating the rule rather
than the list.* than the list.*
@@ -412,23 +412,23 @@ have a route and one does not:
| The controller | It is what installs modules. Nothing can install it before it runs. | **Carried inside the installer** and published once there is a registry (ADR 0067) | | The controller | It is what installs modules. Nothing can install it before it runs. | **Carried inside the installer** and published once there is a registry (ADR 0067) |
| The registry | It *is* where artifacts are delivered from. A store cannot be delivered through itself. | **Pulled from the public internet** — its image is an ordinary public one, never built ([`04-ISSUES/029`](../../04-ISSUES/029-the-artifact-store-cannot-be-delivered-by-the-artifact-store/00-report.md)) | | The registry | It *is* where artifacts are delivered from. A store cannot be delivered through itself. | **Pulled from the public internet** — its image is an ordinary public one, never built ([`04-ISSUES/029`](../../04-ISSUES/029-the-artifact-store-cannot-be-delivered-by-the-artifact-store/00-report.md)) |
| The builder | It is what builds. Nothing builds it before it runs. | Built at genesis by the init builder (ADR 0070) | | The builder | It is what builds. Nothing builds it before it runs. | Built at genesis by the init builder (ADR 0070) |
| The catalogue | It owns the module graph, and nothing can be resolved or installed without it. | Built at genesis by the init builder (ADR 0070) | | The catalog | It owns the module graph, and nothing can be resolved or installed without it. | Built at genesis by the init builder (ADR 0070) |
**How they arrive is settled and not yet built.** The installer carries an init builder, which **How they arrive is settled and not yet built.** The installer carries an init builder, which
clones the source and builds the controller, the catalogue and the builder before a mesh exists clones the source and builds the controller, the catalog and the builder before a mesh exists
to install anything. Two things about that are open and named in to install anything. Two things about that are open and named in
ADR 0070: where the init builder ADR 0070: where the init builder
clones from, given the forge normally runs on the mesh it would be rebuilding, and what it clones from, given the forge normally runs on the mesh it would be rebuilding, and what it
publishes into, given the registry is installed later in the order today. publishes into, given the registry is installed later in the order today.
**There is a fourth, and the list was closed too early.** The catalogue owns the module graph **There is a fourth, and the list was closed too early.** The catalog owns the module graph
(ADR 0070), so it cannot be (ADR 0070), so it cannot be
installed by something that needs it in order to install anything — the same shape as the other installed by something that needs it in order to install anything — the same shape as the other
three. This section previously said the list was closed at three, which was written before the three. This section previously said the list was closed at three, which was written before the
catalogue had an owner and is corrected here rather than left to be reasoned from. catalog had an owner and is corrected here rather than left to be reasoned from.
**And the answer for all four is now one mechanism, not four special cases.** Genesis carries an **And the answer for all four is now one mechanism, not four special cases.** Genesis carries an
*init builder* and builds the core modules on the machine — controller, catalogue and builder — *init builder* and builds the core modules on the machine — controller, catalog and builder —
rather than carrying a finished image of any of them. So the question is no longer "how does this rather than carrying a finished image of any of them. So the question is no longer "how does this
one get here first?" asked once per component; it is answered once, by the thing that is carried one get here first?" asked once per component; it is answered once, by the thing that is carried
being a builder rather than a result. being a builder rather than a result.
@@ -102,7 +102,7 @@ anything on the machine that can talk to the runtime. So a secret reaches a proc
the file the node-engine wrote and points the program at it, and the mesh's own programs take a `_FILE` the file the node-engine wrote and points the program at it, and the mesh's own programs take a `_FILE`
twin for every variable that carries a credential. Software that reads only its environment is twin for every variable that carries a credential. Software that reads only its environment is
not forbidden; it is **declared**, on the container, with a reason, so the manifest says which not forbidden; it is **declared**, on the container, with a reason, so the manifest says which
secrets are exposed that way. **Checked** at composition: the catalogue engine refuses a secrets are exposed that way. **Checked** at composition: the catalog engine refuses a
`${secret:…}` in a container's `env` outright, and a secret-carrying file named in `env-file` `${secret:…}` in a container's `env` outright, and a secret-carrying file named in `env-file`
unless the container carries the declaration. unless the container carries the declaration.
+1 -1
View File
@@ -170,7 +170,7 @@ the one the mesh runs for its own: it offers a place to push, and claims that ro
machine. machine.
**A rule was enforced only at the far end.** A module may not declare an action, and the node-engine **A rule was enforced only at the far end.** A module may not declare an action, and the node-engine
refused one correctly — but the controller accepted it into the catalogue, resolved it and refused one correctly — but the controller accepted it into the catalog, resolved it and
pushed it, so the refusal arrived on a machine with nothing tying it back to the manifest. The pushed it, so the refusal arrived on a machine with nothing tying it back to the manifest. The
rule held; it was just unusable, which is the same shape as the network shape that cost five rule held; it was just unusable, which is the same shape as the network shape that cost five
failing tests before anyone read the node-engine's log. It is now refused where it is written. failing tests before anyone read the node-engine's log. It is now refused where it is written.
+6 -6
View File
@@ -132,7 +132,7 @@ adopted as modules, and the machine's service is still reachable.
Genesis ends with a mesh of one that runs, and that is not the same as a mesh that works. What it Genesis ends with a mesh of one that runs, and that is not the same as a mesh that works. What it
has is a controller, a store, a queue and a registry. What it cannot yet do is **produce has is a controller, a store, a queue and a registry. What it cannot yet do is **produce
anything** — and almost every module in the catalogue is waiting to be produced, because a manifest anything** — and almost every module in the catalog is waiting to be produced, because a manifest
names what its artifacts are and nothing has made them. names what its artifacts are and nothing has made them.
So installing continues: So installing continues:
@@ -172,7 +172,7 @@ need no registry. The gitea module's own server is an upstream image, so buildin
**The store genesis raises is the `postgres` module's server, so adopting it keeps it** (ADR 0338, the **The store genesis raises is the `postgres` module's server, so adopting it keeps it** (ADR 0338, the
store's twin of the bus's ADR 0333). The bundle raises the store under the name, on the image, in the data store's twin of the bus's ADR 0333). The bundle raises the store under the name, on the image, in the data
directory and with the superuser password file that the catalogue's `postgres` module declares, and every step directory and with the superuser password file that the catalog's `postgres` module declares, and every step
of the bundle and of the installer reaches it by that name. At the store step the module declares the of the bundle and of the installer reaches it by that name. At the store step the module declares the
container the bundle raised, so the store is adopted where it runs and is not restarted. The module does not container the bundle raised, so the store is adopted where it runs and is not restarted. The module does not
change, so no running mesh's store moves. *How it is checked:* a test reads the module's manifest and holds change, so no running mesh's store moves. *How it is checked:* a test reads the module's manifest and holds
@@ -190,7 +190,7 @@ something it made rather than by a placeholder.
mesh running; building it from its own repository and path is what makes it upgradeable. The first mesh running; building it from its own repository and path is what makes it upgradeable. The first
time that happens is the moment the mesh stops depending on the installer for anything. time that happens is the moment the mesh stops depending on the installer for anything.
**And then the catalogue.** Every module with source of its own is built the same way. Until this **And then the catalog.** Every module with source of its own is built the same way. Until this
has happened a mesh can install only what is public or carried, which is the foundation and little has happened a mesh can install only what is public or carried, which is the foundation and little
else. else.
@@ -233,7 +233,7 @@ was brought up the supported way, so the rule that it must be is, today, unenfor
**Nothing asks for the core modules yet.** Installing ends with a builder that works — a raised **Nothing asks for the core modules yet.** Installing ends with a builder that works — a raised
machine will build the shared base and a module on top of it when asked — and nothing asks. The machine will build the shared base and a module on top of it when asked — and nothing asks. The
paragraphs above describing the catalogue being built are a thing somebody now types, rather than a paragraphs above describing the catalog being built are a thing somebody now types, rather than a
thing that cannot happen. thing that cannot happen.
**A module's declaration still has to be copied onto the machine by hand.** The installer reads the **A module's declaration still has to be copied onto the machine by hand.** The installer reads the
@@ -242,7 +242,7 @@ longer among them: it lives in the controller's own repository
(ADR 0069), the build the (ADR 0069), the build the
installer runs reports it with the artifact resolved, and genesis registers that installer runs reports it with the artifact resolved, and genesis registers that
([issue 072](../../04-ISSUES/072-the-controllers-manifest-exists-twice/00-report.md)). The ([issue 072](../../04-ISSUES/072-the-controllers-manifest-exists-twice/00-report.md)). The
builder's manifest belongs in the same repository and is still read from the catalogue; that is builder's manifest belongs in the same repository and is still read from the catalog; that is
the last of this. the last of this.
**A machine has no account for a registry that asks for one.** The mesh grants a consumer a **A machine has no account for a registry that asks for one.** The mesh grants a consumer a
@@ -275,7 +275,7 @@ issue tracker holds it as issue 602.
| An image is named exactly | A machine refuses a bundle naming an image by tag. The refusal is exercised, not assumed. | | An image is named exactly | A machine refuses a bundle naming an image by tag. The refusal is exercised, not assumed. |
| The installer is what installed this | **Nothing.** See above. | | The installer is what installed this | **Nothing.** See above. |
| The builder can arrive on a fresh mesh | The installer carries it and installs it as its last step, and the genesis bed raises a machine by running the installer. A bed that raises one any other way fails its own acceptance check. | | The builder can arrive on a fresh mesh | The installer carries it and installs it as its last step, and the genesis bed raises a machine by running the installer. A bed that raises one any other way fails its own acceptance check. |
| The builder is assigned before the package registry exists | The build-agent manifest's own test holds the requirement optional and the genesis document declared and issued outside the mesh; the controller's resolution test over the real catalogue assigns the builder with no provider, lists the requirement unbound, and binds it once gitea is offered. The builder's tests refuse a package, an `.npmrc` image and an `@novox/` TypeScript build with neither binding nor genesis document, before anything is built, and build a Go artifact; they read the genesis document only without a binding. The installer's test seals the document at the packages step and gives the builder no `serves` setting and no `npm-password`. **Not yet proved live:** a genesis bed from bare passing the builder, packages, sdk and base steps. | | The builder is assigned before the package registry exists | The build-agent manifest's own test holds the requirement optional and the genesis document declared and issued outside the mesh; the controller's resolution test over the real catalog assigns the builder with no provider, lists the requirement unbound, and binds it once gitea is offered. The builder's tests refuse a package, an `.npmrc` image and an `@novox/` TypeScript build with neither binding nor genesis document, before anything is built, and build a Go artifact; they read the genesis document only without a binding. The installer's test seals the document at the packages step and gives the builder no `serves` setting and no `npm-password`. **Not yet proved live:** a genesis bed from bare passing the builder, packages, sdk and base steps. |
| The controller a mesh runs is one it built | The genesis bed asserts the running controller is pinned to a digest this mesh's own registry serves, for an image built from a named repository and commit — not one the installer carried. | | The controller a mesh runs is one it built | The genesis bed asserts the running controller is pinned to a digest this mesh's own registry serves, for an image built from a named repository and commit — not one the installer carried. |
| A core module is built rather than only carried | The controller is rebuilt from its own repository and path, and the running mesh is upgraded to the result — the same path any module takes. | | A core module is built rather than only carried | The controller is rebuilt from its own repository and path, and the running mesh is upgraded to the result — the same path any module takes. |
| Installing produced a mesh that can produce | A module with source of its own is asked for and comes back pinned to a digest this mesh's registry assigned, not to a placeholder. **Done by hand on a raised machine, not yet by a bed** — it built the shared base and then a module naming that base. Nothing automated asserts it, which makes this the weakest check on this page. | | Installing produced a mesh that can produce | A module with source of its own is asked for and comes back pinned to a digest this mesh's registry assigned, not to a placeholder. **Done by hand on a raised machine, not yet by a bed** — it built the shared base and then a module naming that base. Nothing automated asserts it, which makes this the weakest check on this page. |
+9 -9
View File
@@ -43,7 +43,7 @@ Turning a module's source into artifacts the mesh can pin, publish and deliver
truthfully what was produced and what it was produced against. truthfully what was produced and what it was produced against.
Everything else is somebody else's: *what* to build is the controller's, *what a build means* is Everything else is somebody else's: *what* to build is the controller's, *what a build means* is
the catalogue's (ADR 0072), *where an artifact runs* is the the catalog's (ADR 0072), *where an artifact runs* is the
controller's again. The builder's whole responsibility is the middle. controller's again. The builder's whole responsibility is the middle.
## The language ## The language
@@ -83,7 +83,7 @@ output into a second stage; and sets an environment variable naming the compiled
module repeats it. Miss any step and the failure arrives somewhere else — as a crash loop, a module repeats it. Miss any step and the failure arrives somewhere else — as a crash loop, a
placeholder digest, a module that builds and does nothing. placeholder digest, a module that builds and does nothing.
The evidence that this is too hard is in the catalogue: **most modules are not converted**, and the The evidence that this is too hard is in the catalog: **most modules are not converted**, and the
two converted during one session were each wrong twice before they were right, against a working two converted during one session were each wrong twice before they were right, against a working
example sitting open in the next window. example sitting open in the next window.
@@ -163,8 +163,8 @@ makes the drift worse while hiding it.
| A module with its own code needs no Dockerfile | A module declaring only a language builds, and its artifact is pinned to a digest the mesh's registry assigned. | | A module with its own code needs no Dockerfile | A module declaring only a language builds, and its artifact is pinned to a digest the mesh's registry assigned. |
| An archive is produced and delivered | A module declaring an archive is built, and the machine assigned it has the unpacked files — verified on the machine, not in the build's own output. | | An archive is produced and delivered | A module declaring an archive is built, and the machine assigned it has the unpacked files — verified on the machine, not in the build's own output. |
| A Dockerfile still works | A module that declares one builds exactly as before, because software that needs a particular base has not stopped existing. | | A Dockerfile still works | A module that declares one builds exactly as before, because software that needs a particular base has not stopped existing. |
| Nothing is announced that was not made | A build made to fail announces nothing, and the catalogue's graph is unchanged afterwards. | | Nothing is announced that was not made | A build made to fail announces nothing, and the catalog's graph is unchanged afterwards. |
| A build keeps what it was told | A catalogue started after a build asks for what it missed and receives the manifest and the artifacts stood on, not a summary. | | A build keeps what it was told | A catalog started after a build asks for what it missed and receives the manifest and the artifacts stood on, not a summary. |
| The toolchain is the mesh's, not the author's | Changing the toolchain makes every module built against it stale, and each names what moved. | | The toolchain is the mesh's, not the author's | Changing the toolchain makes every module built against it stale, and each names what moved. |
## The whole surface, in one place ## The whole surface, in one place
@@ -207,7 +207,7 @@ declared in one of three ways, for the three things a path can be: the module's
or file resource, or where a secret, grant or contribution lands), the operator's (an `accesses` or file resource, or where a secret, grant or contribution lands), the operator's (an `accesses`
entry), or the machine's (a facility a declared capability grants — `container-runtime` grants its entry), or the machine's (a facility a declared capability grants — `container-runtime` grants its
socket). *How it is checked:* the parser refuses an undeclared mount naming the path and the three socket). *How it is checked:* the parser refuses an undeclared mount naming the path and the three
remedies, and a test parses every manifest in the catalogue beside the checkout. remedies, and a test parses every manifest in the catalog beside the checkout.
### What it builds ### What it builds
@@ -247,7 +247,7 @@ counts as a copy and what as a base.
| `directory` | a directory with a mode and an owner, **placed by the mesh** under the node's root: `place: "."` is the assignment's own root, `place: "mesh"` the mesh's directory for the module, a pathless one sits beneath the root by its id; a stated path is the placement for data that must stay where it is, and may itself sit beneath a placed one (`${dir:<id>}/…`). Everything else names it as `${dir:<id>}` ([issue 119](../../04-ISSUES/119-a-module-definition-decides-where-its-files-live/00-report.md), [174](../../04-ISSUES/174-the-meshs-own-files-for-a-module-are-placed-by-the-definition/00-report.md)) | ✅ | | `directory` | a directory with a mode and an owner, **placed by the mesh** under the node's root: `place: "."` is the assignment's own root, `place: "mesh"` the mesh's directory for the module, a pathless one sits beneath the root by its id; a stated path is the placement for data that must stay where it is, and may itself sit beneath a placed one (`${dir:<id>}/…`). Everything else names it as `${dir:<id>}` ([issue 119](../../04-ISSUES/119-a-module-definition-decides-where-its-files-live/00-report.md), [174](../../04-ISSUES/174-the-meshs-own-files-for-a-module-are-placed-by-the-definition/00-report.md)) | ✅ |
| `file` | literal content, with `${bound:…}`, `${secret:…}`, `${dir:…}`, `${port:…}`, `${machine:…}` and `${setting:…}` filled in — the last an operator's value from the assignment's settings, refused by name when unset (ADR 0155) unless the manifest gives a preference a default (ADR 0262) | ✅ | | `file` | literal content, with `${bound:…}`, `${secret:…}`, `${dir:…}`, `${port:…}`, `${machine:…}` and `${setting:…}` filled in — the last an operator's value from the assignment's settings, refused by name when unset (ADR 0155) unless the manifest gives a preference a default (ADR 0262) | ✅ |
| `user` | a login | ✅ | | `user` | a login | ✅ |
| `access` | a pre-existing path it may use and must not own, **named by id** and placed by the assignment (`accesses: {<id>: <path>}` on its settings); mounts say `${access:<id>}`; a path in the definition is the default an assignment replaces, tolerated while the catalogue converts ([issue 153](../../04-ISSUES/153-an-adopted-machines-data-cannot-be-placed-where-it-is/00-report.md)) | ✅ | | `access` | a pre-existing path it may use and must not own, **named by id** and placed by the assignment (`accesses: {<id>: <path>}` on its settings); mounts say `${access:<id>}`; a path in the definition is the default an assignment replaces, tolerated while the catalog converts ([issue 153](../../04-ISSUES/153-an-adopted-machines-data-cannot-be-placed-where-it-is/00-report.md)) | ✅ |
| `archive` | files fetched by digest and unpacked | ✅ | | `archive` | files fetched by digest and unpacked | ✅ |
| `package` | a package that must be present | ✅ | | `package` | a package that must be present | ✅ |
| `network` | a named container network | ✅ | | `network` | a named container network | ✅ |
@@ -292,7 +292,7 @@ This is the bus's shared-work pattern, not a build-specific one: any module decl
`accepts` has its work shared by its holders the same way. Building is the first use. `accepts` has its work shared by its holders the same way. Building is the first use.
*Built and proven live 2026-10-03.* `build-agent` holds `node-build-agent` on all four machines; the first *Built and proven live 2026-10-03.* `build-agent` holds `node-build-agent` on all four machines; the first
build taken by a workstation's agent was a catalogue module at 09:35 UTC; the one-holder `builder` is build taken by a workstation's agent was a catalog module at 09:35 UTC; the one-holder `builder` is
retired. The switch found five gaps, each an issue: a worker whose type changed stranded its holder retired. The switch found five gaps, each an issue: a worker whose type changed stranded its holder
([206](../../04-ISSUES/206-a-seats-worker-changing-type-strands-the-holder-and-the-build-that-would-fix-it/00-report.md)), ([206](../../04-ISSUES/206-a-seats-worker-changing-type-strands-the-holder-and-the-build-that-would-fix-it/00-report.md)),
a re-made worker replayed the stream's history ([207](../../04-ISSUES/207-a-re-made-worker-replayed-every-ask-the-stream-kept/00-report.md)), a re-made worker replayed the stream's history ([207](../../04-ISSUES/207-a-re-made-worker-replayed-every-ask-the-stream-kept/00-report.md)),
@@ -316,7 +316,7 @@ controller's seat verb through the mesh MCP server — reads it back afterwards.
id beside it, and `build` says the id it asked with. The mesh keeps no second copy: the stream is the id beside it, and `build` says the id it asked with. The mesh keeps no second copy: the stream is the
log. The mesh MCP server's `build` tool asks and answers at once with the id; the outcome is taken in — the log. The mesh MCP server's `build` tool asks and answers at once with the id; the outcome is taken in — the
build recorded, the module registered with its source — by whoever hears it, the waiting command or build recorded, the module registered with its source — by whoever hears it, the waiting command or
the daemon following the role's event, so a build nobody waited for still reaches the catalogue the daemon following the role's event, so a build nobody waited for still reaches the catalog
([issue 176](../../04-ISSUES/176-the-consoles-build-tool-neither-waits-nor-registers/00-report.md)). A viewer of builds, when one is built, is a subscriber over these subjects and the outcome; the ([issue 176](../../04-ISSUES/176-the-consoles-build-tool-neither-waits-nor-registers/00-report.md)). A viewer of builds, when one is built, is a subscriber over these subjects and the outcome; the
builder needs nothing more for it. builder needs nothing more for it.
@@ -383,7 +383,7 @@ than retried for ever. Live: the store's size before and after the first nightly
*2026-09-29 — *2026-09-29 —
ADR 0142.* ADR 0142.*
The toolchain list was typescript and python, and only typescript had a base module in the catalogue. The toolchain list was typescript and python, and only typescript had a base module in the catalog.
Meanwhile the controller — written in the language this project is mostly written in — was built as Meanwhile the controller — written in the language this project is mostly written in — was built as
an image from a hand-written Dockerfile, which is the per-repository incantation this whole mechanism an image from a hand-written Dockerfile, which is the per-repository incantation this whole mechanism
exists to abolish. exists to abolish.
+1 -1
View File
@@ -171,7 +171,7 @@ knows. Putting the Plex client anywhere else is the shared-library disease with
- **Which private registry.** That there *is* one is settled — - **Which private registry.** That there *is* one is settled —
ADR 0014 says each module consumes its dependencies ADR 0014 says each module consumes its dependencies
from the private registry, the mesh's own shared library included. Which software serves it is from the private registry, the mesh's own shared library included. Which software serves it is
not: the catalogue holds `verdaccio`, and a git host usually serves package registries too. not: the catalog holds `verdaccio`, and a git host usually serves package registries too.
- **And the bootstrap does not have one.** ADR 0014 assumes a registry exists; on a fresh mesh - **And the bootstrap does not have one.** ADR 0014 assumes a registry exists; on a fresh mesh
nothing has installed one when the first SDK is built. That is the same pivot as everything else nothing has installed one when the first SDK is built. That is the same pivot as everything else
and it has not been designed. and it has not been designed.
@@ -65,14 +65,14 @@ and waits — which looks exactly like a builder with no work.
**Genesis ends with a mesh that runs, which is not the same as a mesh that works.** It has a controller, a store, a broker, a registry and a builder. It holds no module graph, has no private **Genesis ends with a mesh that runs, which is not the same as a mesh that works.** It has a controller, a store, a broker, a registry and a builder. It holds no module graph, has no private
network, no packet filter, and cannot resolve a name. Calling that "installed" is what let the network, no packet filter, and cannot resolve a name. Calling that "installed" is what let the
catalogue be missing from a test for weeks without anything complaining. catalog be missing from a test for weeks without anything complaining.
| # | step | what happens | why it is here | | # | step | what happens | why it is here |
|---|---|---|---| |---|---|---|---|
| 13 | the shared base is built | the toolchain and runtime every module with code of its own stands on | nothing else with code can be built until it exists | | 13 | the shared base is built | the toolchain and runtime every module with code of its own stands on | nothing else with code can be built until it exists |
| 14 | a store module is built and run | a database **provider**, which the foundation's store is not | the foundation's store is the controller's own memory, and offers nothing to anything | | 14 | a store module is built and run | a database **provider**, which the foundation's store is not | the foundation's store is the controller's own memory, and offers nothing to anything |
| 15 | the catalogue is built and run | the module graph | without it the mesh cannot say what it holds, what a change reaches, or what must be rebuilt | | 15 | the catalog is built and run | the module graph | without it the mesh cannot say what it holds, what a change reaches, or what must be rebuilt |
| 16 | the catalogue asks for what it missed | the builds made before it existed are replayed | on a fresh mesh those are always the base, the store and the catalogue itself ([issue 050](../../04-ISSUES/050-the-catalogue-knows-nothing-built-before-it/00-report.md)) | | 16 | the catalog asks for what it missed | the builds made before it existed are replayed | on a fresh mesh those are always the base, the store and the catalog itself ([issue 050](../../04-ISSUES/050-the-catalogue-knows-nothing-built-before-it/00-report.md)) |
| 17 | the controller is rebuilt from its own repository | and rolled out through the module path | the moment the mesh stops depending on the installer for anything | | 17 | the controller is rebuilt from its own repository | and rolled out through the module path | the moment the mesh stops depending on the installer for anything |
| 18 | `mesh-wireguard` is assigned **and the node placed** | a private network | assigning installs the module; placing says where this machine is on it. Both, or the machine has no peers. Assigned by its own name since ADR 0226 retired the `networking` bundle | | 18 | `mesh-wireguard` is assigned **and the node placed** | a private network | assigning installs the module; placing says where this machine is on it. Both, or the machine has no peers. Assigned by its own name since ADR 0226 retired the `networking` bundle |
| 19 | the packet filter is assigned | rules generated from what modules declared | until this, every rule the mesh computes has never been applied to anything | | 19 | the packet filter is assigned | rules generated from what modules declared | until this, every rule the mesh computes has never been applied to anything |
+2 -2
View File
@@ -44,7 +44,7 @@ after the phases that change its build path are in — not before.
**Why here.** The Go controller and the TypeScript SDK disagree about what a grant carries **Why here.** The Go controller and the TypeScript SDK disagree about what a grant carries
(`consumer` is the module in one, the node in the other). That is exercised by the installer's own (`consumer` is the module in one, the node in the other). That is exercised by the installer's own
provisioning — the catalogue's database — so it belongs before more is built on it. provisioning — the catalog's database — so it belongs before more is built on it.
- [x] 1.1 the drift was not live — inspection showed the wire agrees (contributions file; envelope - [x] 1.1 the drift was not live — inspection showed the wire agrees (contributions file; envelope
required headers). The dead types that disagreed are removed, ADR 0074 and doc 19 corrected required headers). The dead types that disagreed are removed, ADR 0074 and doc 19 corrected
@@ -91,7 +91,7 @@ a protocol that agrees, a registry to publish to. Issue 051.
the controller's records and the database of each module that asks for one. *(Adopted in place: the module's the controller's records and the database of each module that asks for one. *(Adopted in place: the module's
server names the same container and the same pinned upstream image the foundation runs, so the server names the same container and the same pinned upstream image the foundation runs, so the
applier reconciles it rather than raising a second postgres. Proven 22/22 in the one-node lab — applier reconciles it rather than raising a second postgres. Proven 22/22 in the one-node lab —
the catalogue, a `postgres-database` consumer, gets its database from it. Follow-up: the store the catalog, a `postgres-database` consumer, gets its database from it. Follow-up: the store
binds `0.0.0.0` from genesis so a mesh consumer can reach it, but the packet filter is binds `0.0.0.0` from genesis so a mesh consumer can reach it, but the packet filter is
installed later — a brief pre-filter window where `mesh-store` is open before `from: mesh` installed later — a brief pre-filter window where `mesh-store` is open before `from: mesh`
clamps it; bring the filter up earlier or bind narrower at genesis.)* clamps it; bring the filter up earlier or bind narrower at genesis.)*
+1 -1
View File
@@ -11,7 +11,7 @@ decisions:
# 23 — Choosing a provider # 23 — Choosing a provider
**Where it stands, 2026-10-17** (the records cleanup, issue 1072; playbook 07, "The design's state is written back"). Built on main: mesh-controller's internal/catalogue/resolve.go implements the model described here, and designs 26, 27 and 29 rely on it. Not yet checked against this text clause by clause, nor given an as-is: issue 1074. **Where it stands, 2026-10-17** (the records cleanup, issue 1072; playbook 07, "The design's state is written back"). Built on main: mesh-controller's `internal/catalogue/resolve.go` implements the model described here, and designs 26, 27 and 29 rely on it. Not yet checked against this text clause by clause, nor given an as-is: issue 1074.
A provision is named for what the consumer's code is coupled to A provision is named for what the consumer's code is coupled to
(ADR 0027): (ADR 0027):
+64 -10
View File
@@ -1,9 +1,10 @@
--- ---
layer: to-be layer: to-be
status: implemented status: in-progress
code: [mesh-catalog, mesh-controller, mesh-host, mesh-media-catalog] code: [mesh-catalog, mesh-controller, mesh-host, mesh-media-catalog, mesh-vault]
updated: 2026-10-12 updated: 2026-10-26
decisions: decisions:
- "ADR 0550"
- "ADR 0305" - "ADR 0305"
- "ADR 0158" - "ADR 0158"
- "ADR 0094" - "ADR 0094"
@@ -31,9 +32,10 @@ wrong.
- **A provisioned credential** is the login one module uses against another — a database password, - **A provisioned credential** is the login one module uses against another — a database password,
a broker account. The mesh mints it per consumer↔provider pair and seals it to the machines that a broker account. The mesh mints it per consumer↔provider pair and seals it to the machines that
must hold it (ADR 0048), must hold it (ADR 0048),
and its provider makes it true. **This is not the vault's**, and the vault does not change it. It and its provider makes it true. This design first left it out of the vault, since it already has an owner
already has an owner and a rotation and a rotation ([13](13-credentials-and-their-rotation.md)). **It is the vault's now** (ADR 0550 §5, from
([13](13-credentials-and-their-rotation.md)); disturbing it would buy nothing. ADR 0113): the vault keeps the value and hands it sealed; its provider still makes it true, and its
rotation is the same machinery.
- **A module's own secret** is a value a single module needs for itself — the password of a store - **A module's own secret** is a value a single module needs for itself — the password of a store
it runs privately, an internal signing token. Today the mesh generates this as a value nothing it runs privately, an internal signing token. Today the mesh generates this as a value nothing
owns, and so it cannot be rotated. **This is the vault's.** owns, and so it cannot be rotated. **This is the vault's.**
@@ -45,6 +47,56 @@ The vault, then, is the provider a module turns to for a secret that is **not**
some other provision. It is the answer to *"this module needs a password, and there is no provider some other provision. It is the answer to *"this module needs a password, and there is no provider
whose job it is to give it one."* whose job it is to give it one."*
## Every secret lives in the vault
*Decided 2026-10-26 (ADR 0550), on the operator's answers; built in the milestones of ADR 0547.*
**The seat `mesh-vault` holds every secret the mesh keeps.** That is every shared secret, the enrolment tokens
among them; the mesh's signing key; every licence's grants and tokens; every bus account's password or, where
the vault has no value, its hash; and every module's own secrets and credentials. A holder gets a value only
sealed to its own key, on request and reply, or asks the vault to act with it, as for a signature. No secret is
kept in a bucket, a store database or a module's own rows. The private keys a node or the operator makes for
itself stay where they are made (ADR 0550 §1).
- **The signing key.** The vault makes a new signing key for the mesh, with the mesh's authority beside it; the
controller's key is never copied out of its database. The vault signs enrolments, node certificates and the
authority's own on request, through its verb `sign`, for the callers its manifest names, and says `signed`
naming the caller, the kind and the digest, never the key. The public halves are in a state of the vault that
every node reads. The key and the authority leave the vault only as the one copy sealed to the operator key,
opened only in the break-glass recovery below, so losing the vault does not lose the key every signature
depends on. Each node trusts the vault's key beside the controller's, one node first, and the controller's no
more once it loses its lease (ADR 0550 §2).
- **Licence grants and tokens.** The refresh tokens, the API key and the access tokens of every licence are vault
secrets; the licence manager keeps which licence goes to which holder, and nothing secret
([39](39-the-anthropic-licence-manager.md), ADR 0550 §3).
- **Bus account passwords.** A bus account's value is the vault's: until ADR 0547's third milestone the controller
hands the vault the value of each account ADR 0374 §1 covers and keeps the others' values as today (ADR 0374
§4), whose hashes the vault adopts; from then the vault mints every one. The agent of the node holding `mesh-broker` hands the vault,
sealed to it, the bcrypt hash of every user of the list the bus loaded; the vault adopts the hash of each user
it holds no value for (the controller's own, the node and device accounts, the module and process accounts its
values do not cover), writes it under `bus-accounts.<user>` and says `account-hashed` with `adopted: true`. It
never touches a key that holds the hash of its own value, and writes an adopted key again when the loaded list
carries another hash for that user. Its tool `secret_own` lists the adopted users and each adoption, and
`account_adopt` adopts now (ADR 0550 §4).
- **Module secrets** stay as the sections below describe them: a module's own secrets, the credentials between a
consumer and its provider, and the values an operator or a backend delivers. A node composing its own modules
asks the vault for them sealed to its key, and no composition carries one in the clear (ADR 0550 §5).
- **Three exceptions until ADR 0547's third milestone, each ending there:** the controller's own signing key in
its identity database; the bus account values the controller mints, handed to the vault where the accounts are
covered and kept by the controller for the others; and the enrolment tokens and new node accounts the
controller makes and keeps in its store. So **no machine is enrolled before the third milestone** (ADR 0550 §1,
§6).
*How it is checked:* before the third milestone's lease write, the controller's licence and identity databases and
the licence manager hold no secret the vault does not hold, and no exception is left that the vault has not
replaced, read with the vault's `secret_holders` and the store's reader. A unit test that `sign` refuses a caller
its manifest does not name and that no answer or event carries the key; `secret_holders` showing the
operator-sealed copy of the key and no other holder; and live, a node certificate the vault signed that another
node verifies. After the delivery of the hash hand-over, `mesh-grants.grants_user_list` shows no user without a
hash and `secret_own` lists the adopted users; unit tests in the vault (adopt; never touch its own value or a
covered account; rewrite on a new loaded hash) and in the node agent (every loaded user handed, sealed). The
hand-act log shows no enrolment before the third milestone.
## A secret as a provision ## A secret as a provision
A module that needs a secret for its own use requires a `secret` provision, exactly as it requires A module that needs a secret for its own use requires a `secret` provision, exactly as it requires
@@ -77,8 +129,10 @@ the foundation: installed at genesis, the way the store and broker are raised fi
adopted as modules (ADR 0078). A adopted as modules (ADR 0078). A
mesh has root secrets, so a mesh has a vault; it is not something a mesh opts into mesh has root secrets, so a mesh has a vault; it is not something a mesh opts into
(ADR 0085, amended). It is provisioned the (ADR 0085, amended). It is provisioned the
ordinary way, and it does **not** mint the mesh's delivery credentials — the controller does that, ordinary way. It does not yet mint the mesh's bus accounts: until the third milestone of ADR 0547 the
because the controller must mint in order to deliver any provision, the vault's own included. controller mints them, hands the vault each covered account's value (ADR 0374 §1) and keeps the others as
today, and from then the vault mints every one
(*Every secret lives in the vault*, above).
There is **one vault per mesh**, on the control-node, reached from any node the way identity is. There is **one vault per mesh**, on the control-node, reached from any node the way identity is.
Node scoping (ADR 0084, Node scoping (ADR 0084,
@@ -131,7 +185,7 @@ recovery addresses both alike.
### A provider with one credential ### A provider with one credential
*Decided 2026-10-01 (ADR 0158); the controller's half built the same day (mesh-controller PR 184): the offer's word, the need carrying the shared secret's name, the vault's one value under one generation stamp, remade for every holder on a later binding or a rotation, the rotate command sending every holder. What remains is each provider's definition saying `credential` and `taken`, with a start that applies the file — the media catalogue's work.* *Decided 2026-10-01 (ADR 0158); the controller's half built the same day (mesh-controller PR 184): the offer's word, the need carrying the shared secret's name, the vault's one value under one generation stamp, remade for every holder on a later binding or a rotation, the rotate command sending every holder. What remains is each provider's definition saying `credential` and `taken`, with a start that applies the file — the media catalog's work.*
Software that holds one credential — a download client's web password, an indexer's one API key — Software that holds one credential — a download client's web password, an indexer's one API key —
cannot give each consumer a login, so ADR 0048's form does not fit it and its values were accepted cannot give each consumer a login, so ADR 0048's form does not fit it and its values were accepted
@@ -148,7 +202,7 @@ acceptance. *How it is checked:* the rows of ADR 0158's table; the controller's
### A credential the provider's software issued ### A credential the provider's software issued
*Decided 2026-10-12 (ADR 0305, issue 526), accepted on the operator's approval the same day; built on *Decided 2026-10-12 (ADR 0305, issue 526), accepted on the operator's approval the same day; built on
branches in the controller and the media catalogue.* branches in the controller and the media catalog.*
Some software is given its one credential rather than told it: a media server signs in to its vendor and Some software is given its one credential rather than told it: a media server signs in to its vendor and
keeps the token the vendor issued. The mesh can neither make nor rotate such a value, and a copy typed for keeps the token the vendor issued. The mesh can neither make nor rotate such a value, and a copy typed for
+5 -5
View File
@@ -129,7 +129,7 @@ mesh already has a shape for that — a seat's `accept` subjects, on a work queu
holders, which is what a build queue shared by several machines *is*. Keeping a second mechanism for it holders, which is what a build queue shared by several machines *is*. Keeping a second mechanism for it
meant two things to reason about and two places for a permission to be wrong. A build's outcome is the meant two things to reason about and two places for a permission to be wrong. A build's outcome is the
seat's own event — which is why the control branch loses its copy too: one publish reaches whoever seat's own event — which is why the control branch loses its copy too: one publish reaches whoever
asked, the controller that records it and the catalogue that places it — the fan-out a shared exchange gave for free, as a derived subject rather than a asked, the controller that records it and the catalog that places it — the fan-out a shared exchange gave for free, as a derived subject rather than a
configured topology. configured topology.
**Revised 2026-09-26** ([design 29](32-what-a-module-declares.md)): a module's events and tools **Revised 2026-09-26** ([design 29](32-what-a-module-declares.md)): a module's events and tools
@@ -179,7 +179,7 @@ Core NATS is at-most-once. Everything the mesh must not lose lives in a JetStrea
| EVENTS | `mesh.mod.*.event.>` and `mesh.seat.*.event.>`; and `mesh.again.>` (ADR 0264) | limits (age, size), durable consumer per subscribing module | a subscriber that was down catches up; after `max-deliver` attempts the advisory feeds `mesh.events.dead` (its own small stream). *2026-10-08, ADR 0264:* that stream is DEAD_LETTERS, below; each consumer also filters `mesh.again.<consumer>.>`, where a message it gave up on is delivered again to it alone. *2026-10-01:* a build's whole log is here too, as the build seat's `log.<build id>` events (ADR 0157) — one subject per build, a week of retention, read back by `builds --log <id>` with a consumer that is gone when the reading is done | | EVENTS | `mesh.mod.*.event.>` and `mesh.seat.*.event.>`; and `mesh.again.>` (ADR 0264) | limits (age, size), durable consumer per subscribing module | a subscriber that was down catches up; after `max-deliver` attempts the advisory feeds `mesh.events.dead` (its own small stream). *2026-10-08, ADR 0264:* that stream is DEAD_LETTERS, below; each consumer also filters `mesh.again.<consumer>.>`, where a message it gave up on is delivered again to it alone. *2026-10-01:* a build's whole log is here too, as the build seat's `log.<build id>` events (ADR 0157) — one subject per build, a week of retention, read back by `builds --log <id>` with a consumer that is gone when the reading is done |
| DEAD_LETTER_NOTICES | the server's maximum-deliveries advisory | work queue, a week; one consumer (the controller), no `max-deliver` | the server's word that a consumer gave up on a message, kept whether or not a controller is listening, until the controller has copied the message (ADR 0264) | | DEAD_LETTER_NOTICES | the server's maximum-deliveries advisory | work queue, a week; one consumer (the controller), no `max-deliver` | the server's word that a consumer gave up on a message, kept whether or not a controller is listening, until the controller has copied the message (ADR 0264) |
| DEAD_LETTERS | `mesh.events.dead.<stream>.<consumer>` | limits: no age, 256 MiB, refuses new when full; a day's de-duplication | every message a consumer gave up on, with its consumer, subject, deliveries and when, until a person delivers it again or drops it with why (ADR 0264); a `max-deliveries` condition is open per consumer while it holds any | | DEAD_LETTERS | `mesh.events.dead.<stream>.<consumer>` | limits: no age, 256 MiB, refuses new when full; a day's de-duplication | every message a consumer gave up on, with its consumer, subject, deliveries and when, until a person delivers it again or drops it with why (ADR 0264); a `max-deliveries` condition is open per consumer while it holds any |
| `KV_<module>_<name>` | `$KV.<module>_<name>.>` | the newest value per key — as many past values as the owner declared — no age unless the owner declared one; a value at most 256 KiB, a bucket at most 64 MiB | a module's state (ADR 0201): one per name in a manifest's `state`, created from the catalogue on every raise, so it exists before its owner runs anywhere; kept when the module is unassigned, because what it holds is data | | `KV_<module>_<name>` | `$KV.<module>_<name>.>` | the newest value per key — as many past values as the owner declared — no age unless the owner declared one; a value at most 256 KiB, a bucket at most 64 MiB | a module's state (ADR 0201): one per name in a manifest's `state`, created from the catalog on every raise, so it exists before its owner runs anywhere; kept when the module is unassigned, because what it holds is data |
Tool calls and heartbeats stay on core NATS: a lost heartbeat is the next heartbeat; a lost tool Tool calls and heartbeats stay on core NATS: a lost heartbeat is the next heartbeat; a lost tool
call is a timeout the caller already handles. call is a timeout the caller already handles.
@@ -321,7 +321,7 @@ signing hierarchy for nothing.
## 5. The broker as a module ## 5. The broker as a module
`nats` is a catalogue module claiming the seat `mesh-broker` `nats` is a catalog module claiming the seat `mesh-broker`
(ADR 0079: the seat (ADR 0079: the seat
is the server, and the server changes). It declares one container (a single binary; **JetStream on a host is the server, and the server changes). It declares one container (a single binary; **JetStream on a host
directory bind, not a named volume** — revision, second review: directory bind, not a named volume** — revision, second review:
@@ -410,7 +410,7 @@ bridged. It is three things:
for anyone else — and nothing on control, nodes or builds. It is issued, sealed to the person's for anyone else — and nothing on control, nodes or builds. It is issued, sealed to the person's
own key, and revoked, like a module's. own key, and revoked, like a module's.
2. **A client that speaks the bus**: a small program on the workstation that connects as that user 2. **A client that speaks the bus**: a small program on the workstation that connects as that user
over TLS, lists tools by asking the catalogue (`mesh.tools.mesh-catalog.catalog_tools`), and over TLS, lists tools by asking the catalog (`mesh.tools.mesh-catalog.catalog_tools`), and
turns each tool into a call — as an MCP server for an agent, and as a command line for a person. turns each tool into a call — as an MCP server for an agent, and as a command line for a person.
It uses the sdk's `Broker` contract on the NATS runtime, so it is the same code path a module's It uses the sdk's `Broker` contract on the NATS runtime, so it is the same code path a module's
tools use, not a second protocol. tools use, not a second protocol.
@@ -606,7 +606,7 @@ find what changed and why.
decides it. decides it.
- The heartbeat interval and the controller's "quiet" threshold on core NATS without persistence — - The heartbeat interval and the controller's "quiet" threshold on core NATS without persistence —
the same numbers as today are proposed. the same numbers as today are proposed.
- Whether the person's client is a catalogue module (runs on an enrolled workstation node) or a - Whether the person's client is a catalog module (runs on an enrolled workstation node) or a
standalone program (runs anywhere with credentials). Both, in that order, is proposed. standalone program (runs anywhere with credentials). Both, in that order, is proposed.
- Leaf nodes: a NATS leaf per machine would make every module's connection local and survive the - Leaf nodes: a NATS leaf per machine would make every module's connection local and survive the
hub's restart. Deliberately out of scope; noted so it is not forgotten. hub's restart. Deliberately out of scope; noted so it is not forgotten.
+7 -7
View File
@@ -98,7 +98,7 @@ nobody argued for is an entry nobody can explain.
**What deserves one** (ADR 0161). A provision the **What deserves one** (ADR 0161). A provision the
mesh's own code dereferences by name is delivered by a mesh seat its provider claims — the store, the mesh's own code dereferences by name is delivered by a mesh seat its provider claims — the store, the
bus, the vault, the artifact store, the catalogue. A provision a module merely offers may have several bus, the vault, the artifact store, the catalog. A provision a module merely offers may have several
providers, and a consumer with several is a person's choice. A fact of the shape *exactly one machine providers, and a consumer with several is a person's choice. A fact of the shape *exactly one machine
is X* — the hub — is not a seat, because a seat is held by a module assignment and points at it; it is is X* — the hub — is not a seat, because a seat is held by a module assignment and points at it; it is
a placement with a capacity of one, kept by the store, refused by name when a second is placed, and a placement with a capacity of one, kept by the store, refused by name when a second is placed, and
@@ -114,7 +114,7 @@ capability is.
ADR 0110): a module ADR 0110): a module
declares its own seats with their protocols, so the seats a mesh has are the mesh's own **plus declares its own seats with their protocols, so the seats a mesh has are the mesh's own **plus
every registered module's**. The set is still closed — a seat named nowhere is refused — but it is every registered module's**. The set is still closed — a seat named nowhere is refused — but it is
computed from the catalogue rather than maintained by hand, which is the property 0110 actually computed from the catalog rather than maintained by hand, which is the property 0110 actually
needed and the table could not keep. needed and the table could not keep.
**And the mesh's own half is data, named for its scope.** Revision, 2026-09-27, reconciling two **And the mesh's own half is data, named for its scope.** Revision, 2026-09-27, reconciling two
@@ -125,7 +125,7 @@ the set out of compiled code into a table the controller owns, so a rename is on
rebuild of everything that names one. rebuild of everything that names one.
So the set has two halves and neither is written out here: the mesh's own, which the controller holds So the set has two halves and neither is written out here: the mesh's own, which the controller holds
as rows, and every registered module's, which is computed from the catalogue. What this document keeps as rows, and every registered module's, which is computed from the catalog. What this document keeps
is what a seat *is* — the rest would be a third copy, stale the first time somebody renamed one, which is what a seat *is* — the rest would be a third copy, stale the first time somebody renamed one, which
is the fault ADR 0122 exists about. is the fault ADR 0122 exists about.
@@ -142,7 +142,7 @@ convention, which later seats departed from.
| `mesh-broker` | — | mesh | `mesh-bus` | the broker carrying the mesh's own bus | | `mesh-broker` | — | mesh | `mesh-bus` | the broker carrying the mesh's own bus |
| `mesh-vault` | — | mesh | `secret` | the vault (in the seed since ADR 0161; the earlier *reserved* named an effect no rule produced) | | `mesh-vault` | — | mesh | `secret` | the vault (in the seed since ADR 0161; the earlier *reserved* named an effect no rule produced) |
| `mesh-artifact-store` | `the-artifact-store` (renamed 2026-09-30, ADR 0156) | mesh | `artifact-store` | the artifact registry | | `mesh-artifact-store` | `the-artifact-store` (renamed 2026-09-30, ADR 0156) | mesh | `artifact-store` | the artifact registry |
| `mesh-catalog` | `the-catalogue` | mesh | — | the catalogue | | `mesh-catalog` | `the-catalogue` | mesh | — | the catalog |
| `mesh-npm-package-registry` | `npm-package-registry` | mesh | `npm-package-registry` | the forge | | `mesh-npm-package-registry` | `npm-package-registry` | mesh | `npm-package-registry` | the forge |
| `mesh-git` | `git` | mesh | `git` | the forge | | `mesh-git` | `git` | mesh | `git` | the forge |
| `mesh-build-machine` | `the-build-machine` | node | — | a builder | | `mesh-build-machine` | `the-build-machine` | node | — | a builder |
@@ -158,7 +158,7 @@ convention, which later seats departed from.
The controller holds **the mesh's own** entries in code, and a test asserts their size and that The controller holds **the mesh's own** entries in code, and a test asserts their size and that
every one names the record that made it a seat. A module's seats are not here and never will be — every one names the record that made it a seat. A module's seats are not here and never will be —
they are read from the catalogue. **This table and they are read from the catalog. **This table and
ADR 0126 govern, and code that ADR 0126 govern, and code that
disagrees is what is wrong.** The implementation in progress predates several disagrees is what is wrong.** The implementation in progress predates several
things here: seats held by assignments rather than claimed by definitions, the `mesh-vault` seat and things here: seats held by assignments rather than claimed by definitions, the `mesh-vault` seat and
@@ -326,7 +326,7 @@ checked as their tables say:
| Rule | Checked by | | Rule | Checked by |
|---|---| |---|---|
| The set is closed, and every mesh entry names its decision | 0118: a unit test on the mesh's own entries. **The refusal of an unknown seat is at registration, not in the parser** — correction of fact, 2026-09-27: a module may hold a seat *another* module declares, which is the point of naming the seat and not its provider, so whether a claimed name exists is a fact about the whole catalogue and a manifest in isolation cannot be judged on it. Registration tests cover an invented name and a name another module declares; the parser still refuses a claim on the mesh's own `mesh-*`/`node-*` namespace and a scope that disagrees with a declaration in the same manifest. | | The set is closed, and every mesh entry names its decision | 0118: a unit test on the mesh's own entries. **The refusal of an unknown seat is at registration, not in the parser** — correction of fact, 2026-09-27: a module may hold a seat *another* module declares, which is the point of naming the seat and not its provider, so whether a claimed name exists is a fact about the whole catalog and a manifest in isolation cannot be judged on it. Registration tests cover an invented name and a name another module declares; the parser still refuses a claim on the mesh's own `mesh-*`/`node-*` namespace and a scope that disagrees with a declaration in the same manifest. |
| The set is derived, and enumerating it is a query | 0118: the overview lists the mesh's own plus every registered module's, asserted against a fixture mesh. | | The set is derived, and enumerating it is a query | 0118: the overview lists the mesh's own plus every registered module's, asserted against a fixture mesh. |
| `mesh-*` is the mesh's, and a module may not declare one | 0118: a registration test refusing a manifest that declares any `mesh-*` seat, naming the prefix. | | `mesh-*` is the mesh's, and a module may not declare one | 0118: a registration test refusing a manifest that declares any `mesh-*` seat, naming the prefix. |
| Two modules cannot declare the same seat | 0118: a registration test; the second is refused and the first untouched. | | Two modules cannot declare the same seat | 0118: a registration test; the second is refused and the first untouched. |
@@ -339,7 +339,7 @@ checked as their tables say:
| `secret` has one provider, the holder of `mesh-vault` | 0161: a second claimant of the seat is refused by name (`CanHold`); *correction of fact, 2026-10-01: no parser rule ever reserved the word, the seat does the work*. | | `secret` has one provider, the holder of `mesh-vault` | 0161: a second claimant of the seat is refused by name (`CanHold`); *correction of fact, 2026-10-01: no parser rule ever reserved the word, the seat does the work*. |
| A singular fact about machines is a placement of capacity one, refused by name | 0161: the private network command's test for a second hub; the store's unique index. | | A singular fact about machines is a placement of capacity one, refused by name | 0161: the private network command's test for a second hub; the store's unique index. |
| A holder of `node-uplink` is the dialect the machine runs | 0161: the node-engine reports `uplink-<manager>` in its profile with every report; a resolution test refuses the other holder naming the capability. | | A holder of `node-uplink` is the dialect the machine runs | 0161: the node-engine reports `uplink-<manager>` in its profile with every report; a resolution test refuses the other holder naming the capability. |
| A seat's holder has the seats it needs beside it: the resolver configuration is refused at `assign` without the uplink held on its node, and the uplink's last holder cannot be taken from beneath it | 0220: seat-dependency tests on the definition, on a synthetic claimant, at assign, at unassign and at composition, and one reading the catalogue for the uplink's possible holders. | | A seat's holder has the seats it needs beside it: the resolver configuration is refused at `assign` without the uplink held on its node, and the uplink's last holder cannot be taken from beneath it | 0220: seat-dependency tests on the definition, on a synthetic claimant, at assign, at unassign and at composition, and one reading the catalog for the uplink's possible holders. |
| A replicated seat has every holder on record and composes on each; a seat held once still refuses a second holder, on record or not; `--add` is refused for it | 0223: resolution and composition tests with two holders on record and a third machine, with two claimants and nothing on record, and on `mesh-store`; a unit test that only `mesh-dns-resolver` is replicated, surviving the store's rows; store tests keeping several holders once each, a handover leaving one, and unassigning one taking only its row. | | A replicated seat has every holder on record and composes on each; a seat held once still refuses a second holder, on record or not; `--add` is refused for it | 0223: resolution and composition tests with two holders on record and a third machine, with two claimants and nothing on record, and on `mesh-store`; a unit test that only `mesh-dns-resolver` is replicated, surviving the store's rows; store tests keeping several holders once each, a handover leaving one, and unassigning one taking only its row. |
| A retired seat leaves the set once nothing claims it, from the compiled set and the store's table both | 0220: the closed-set test's count, and the store migration that deletes the row. | | A retired seat leaves the set once nothing claims it, from the compiled set and the store's table both | 0220: the closed-set test's count, and the store migration that deletes the row. |
| A node seat names only literal absolute paths; every path has one seat naming it outright, and stepping back does not cycle | 0287: a unit test over the compiled set; registration tests refusing a module's seat naming a path at mesh scope, with `${`, or already named by another seat. | | A node seat names only literal absolute paths; every path has one seat naming it outright, and stepping back does not cycle | 0287: a unit test over the compiled set; registration tests refusing a module's seat naming a path at mesh scope, with `${`, or already named by another seat. |
@@ -47,7 +47,7 @@ A requirement has three parts:
**A contract is shared, not per module.** A database's contract is the database's, whoever requires **A contract is shared, not per module.** A database's contract is the database's, whoever requires
it. **The controller holds every contract**, one per provision name, declared where the provision is it. **The controller holds every contract**, one per provision name, declared where the provision is
defined in the catalogue. Today contracts are implicit in each provider's served fields; the first defined in the catalog. Today contracts are implicit in each provider's served fields; the first
phase below makes them explicit, because nothing can be checked against a contract that is not phase below makes them explicit, because nothing can be checked against a contract that is not
written down. A provider is checked against the contract it claims to answer. A module's own written down. A provider is checked against the contract it claims to answer. A module's own
specification may narrow a contract (a password of at least this length, a directory owned by this specification may narrow a contract (a password of at least this length, a directory owned by this
@@ -213,13 +213,13 @@ a placed directory under the node's root. Each is this design's provider in the
placeholders have, not yet the one requirement form below; they are phase 1's first cases. placeholders have, not yet the one requirement form below; they are phase 1's first cases.
*Phase 3, in part (2026-09-30):* every definition's **own** data directory is placed; the conversion *Phase 3, in part (2026-09-30):* every definition's **own** data directory is placed; the conversion
moved no data, proven by resolving both catalogues with the controller's rule and comparing moved no data, proven by resolving both catalogs with the controller's rule and comparing
([issue 119](../../04-ISSUES/119-a-module-definition-decides-where-its-files-live/00-report.md)). ([issue 119](../../04-ISSUES/119-a-module-definition-decides-where-its-files-live/00-report.md)).
What the mesh writes *for* a module was still placed by the definition What the mesh writes *for* a module was still placed by the definition
([issue 174](../../04-ISSUES/174-the-meshs-own-files-for-a-module-are-placed-by-the-definition/00-report.md)), ([issue 174](../../04-ISSUES/174-the-meshs-own-files-for-a-module-are-placed-by-the-definition/00-report.md)),
the gap this design answered on 2026-09-26; built later the same day: a directory saying the gap this design answered on 2026-09-26; built later the same day: a directory saying
`place: "mesh"` is `<root>/mesh/<module>`, a directory beneath a placed one states its path as `place: "mesh"` is `<root>/mesh/<module>`, a directory beneath a placed one states its path as
`${dir:<id>}/<rest>` and moves with it, and the same proof — both catalogues resolved and compared — `${dir:<id>}/<rest>` and moves with it, and the same proof — both catalogs resolved and compared —
shows forty-eight definitions naming the paths they named before. shows forty-eight definitions naming the paths they named before.
*A preference has a default; the operator's own value has none (2026-10-08, *A preference has a default; the operator's own value has none (2026-10-08,
@@ -323,7 +323,7 @@ value in a container's environment is refused when the definition is parsed, wit
containers, login, broker account and settings are keyed by it, as today, and a login still fits the containers, login, broker account and settings are keyed by it, as today, and a login still fits the
backend that keeps it (ADR 0049): backend that keeps it (ADR 0049):
the bound is the one the offer of each provision it requires states, a keyless provision states none, the bound is the one the offer of each provision it requires states, a keyless provision states none,
and an overflow is refused by the catalogue check before merge and left out of the provider's grants, and an overflow is refused by the catalog check before merge and left out of the provider's grants,
reported, at composition — never a refusal of the provider's machine reported, at composition — never a refusal of the provider's machine
(ADR 0225). (ADR 0225).
@@ -371,7 +371,7 @@ not rotated by the vault, which cannot make its replacement: an operator deliver
**Each requirement says how its recipient takes a new value.** It either *applies* it, through a **Each requirement says how its recipient takes a new value.** It either *applies* it, through a
provisioner (a provider setting a login's password, the broker's provisioner updating an account, a provisioner (a provider setting a login's password, the broker's provisioner updating an account, a
store's provisioner changing its own superuser), or *reads it at start*. Every module in the catalogue store's provisioner changing its own superuser), or *reads it at start*. Every module in the catalog
reads its secrets at start, and none watches them. The node-engine already recreates a container when a file reads its secrets at start, and none watches them. The node-engine already recreates a container when a file
it read at creation changes, its env-files and files mounted into it directly it read at creation changes, its env-files and files mounted into it directly
([issue 103](../../04-ISSUES/103-a-container-is-not-recreated-when-a-file-it-reads-changes/00-report.md)). ([issue 103](../../04-ISSUES/103-a-container-is-not-recreated-when-a-file-it-reads-changes/00-report.md)).
@@ -433,7 +433,7 @@ as waiting, and nothing is delivered until the answer arrives.
| paths in resources, mounts, bindings, secrets and received files | host directory requirements, placed by the assignment | | paths in resources, mounts, bindings, secrets and received files | host directory requirements, placed by the assignment |
| literals carried in a definition | operator requirements with defaults | | literals carried in a definition | operator requirements with defaults |
Each is retired only once nothing uses it. Until then both are accepted, and a catalogue test lists Each is retired only once nothing uses it. Until then both are accepted, and a catalog test lists
the definitions still using the old form. That list shrinks to empty, and then the old form is the definitions still using the old form. That list shrinks to empty, and then the old form is
removed from the parser. removed from the parser.
@@ -453,7 +453,7 @@ Each phase ends at a check that holds, so none of them leaves a mechanism half-r
is fixed in the node-engine already. *Ends when* nothing outside the vault generates a shared secret after is fixed in the node-engine already. *Ends when* nothing outside the vault generates a shared secret after
genesis, a lab consumer of analytics receives its site id, and a database credential rotates over genesis, a lab consumer of analytics receives its site id, and a database credential rotates over
its two credentials, the consumer recreated by derivation, never without a working login, and its data intact. its two credentials, the consumer recreated by derivation, never without a working login, and its data intact.
3. **Definitions move, and seats move to assignments.** Every catalogue definition is rewritten, adopted 3. **Definitions move, and seats move to assignments.** Every catalog definition is rewritten, adopted
and running assignments placed where their data already is, and each claim becomes a seat the and running assignments placed where their data already is, and each claim becomes a seat the
module can hold, held by the assignment that holds it today. *Ends when* the list of definitions module can hold, held by the assignment that holds it today. *Ends when* the list of definitions
using an old form is empty, the old forms are removed, and the store module runs on two lab using an old form is empty, the old forms are removed, and the store module runs on two lab
@@ -464,7 +464,7 @@ Each phase ends at a check that holds, so none of them leaves a mechanism half-r
| Rule | Checked by | | Rule | Checked by |
|---|---| |---|---|
| Every requirement has one of the four provider kinds | The parser refuses any other. | | Every requirement has one of the four provider kinds | The parser refuses any other. |
| A definition names no host path, node or mesh | The catalogue tests of ADR 0112. | | A definition names no host path, node or mesh | The catalog tests of ADR 0112. |
| A module provider is chosen by named seat, pin, co-location, only one, a person's choice | Resolution tests for each step: a requirement naming a seat served by its holder even with another provider on the consumer's node, and refused when the seat is unheld; several candidates and none local, where assignment lists them with the seat's holder first and records the choice as a pin, and refuses without one. | | A module provider is chosen by named seat, pin, co-location, only one, a person's choice | Resolution tests for each step: a requirement naming a seat served by its holder even with another provider on the consumer's node, and refused when the seat is unheld; several candidates and none local, where assignment lists them with the seat's holder first and records the choice as a pin, and refuses without one. |
| A provider answers within its contract | A controller test: an answer carrying a field its contract does not name, or missing one it does, is refused and not delivered. | | A provider answers within its contract | A controller test: an answer carrying a field its contract does not name, or missing one it does, is refused and not delivered. |
| A module narrows a contract and never widens it | The parser refuses a module specification that loosens a contract's field. | | A module narrows a contract and never widens it | The parser refuses a module specification that loosens a contract's field. |
@@ -484,9 +484,9 @@ Each phase ends at a check that holds, so none of them leaves a mechanism half-r
| The controller and a node's host take the same path | 0113's tests: the controller's definition declares requirements and no own secret; a node's bus account is made by the vault and delivered sealed to that node. | | The controller and a node's host take the same path | 0113's tests: the controller's definition declares requirements and no own secret; a node's bus account is made by the vault and delivered sealed to that node. |
| Moving the vault or the broker is break-glass | A resolution test: an ordinary assignment moving `mesh-vault` or `mesh-broker` is refused, naming the procedure. | | Moving the vault or the broker is break-glass | A resolution test: an ordinary assignment moving `mesh-vault` or `mesh-broker` is refused, naming the procedure. |
| A secret that cannot be rotated says so | A vault test: rotating a secret marked not rotatable by the mesh is refused, naming why. | | A secret that cannot be rotated says so | A vault test: rotating a secret marked not rotatable by the mesh is refused, naming why. |
| Only the controller reads the seat placeholder | A catalogue test, from phase 3: no definition uses the seat placeholder. | | Only the controller reads the seat placeholder | A catalog test, from phase 3: no definition uses the seat placeholder. |
| Refusal names everything at once | A resolution test with three unresolved requirements of different kinds: one refusal naming all three. | | Refusal names everything at once | A resolution test with three unresolved requirements of different kinds: one refusal naming all three. |
| The old forms retire | The catalogue test listing definitions still using one. It must be empty before a form is removed. | | The old forms retire | The catalog test listing definitions still using one. It must be empty before a form is removed. |
## Not settled here ## Not settled here
+14 -14
View File
@@ -169,11 +169,11 @@ paper is wrong until there is a second mesh to find out.
The derivation refuses two things where they can still be named: two users with one name — the The derivation refuses two things where they can still be named: two users with one name — the
server reads the file as one of them and which one depends on the order — and a module server reads the file as one of them and which one depends on the order — and a module
assigned but absent from the catalogue, which would compose a user with no authority and fail assigned but absent from the catalog, which would compose a user with no authority and fail
on its first publish with an authorisation error that says nothing about a missing manifest. on its first publish with an authorisation error that says nothing about a missing manifest.
A user the mesh has minted no password for is *named* rather than dropped or written as a user A user the mesh has minted no password for is *named* rather than dropped or written as a user
anybody is: an ordinary situation with an obvious remedy, and the caller decides whether a anybody is: an ordinary situation with an obvious remedy, and the caller decides whether a
partial file is worth writing. A seat's protocol is gathered across the whole catalogue, not partial file is worth writing. A seat's protocol is gathered across the whole catalog, not
from one manifest, because a seat is declared by one module and held by another. from one manifest, because a seat is declared by one module and held by another.
**Out, and what each needs.** **Out, and what each needs.**
@@ -447,7 +447,7 @@ pays for itself furthest away.
derived to subjects, the three namespaces, permissions computed from a declaration, and a derived to subjects, the three namespaces, permissions computed from a declaration, and a
manifest that contains no subject. Done in the controller's composer (permissions, streams, manifest that contains no subject. Done in the controller's composer (permissions, streams,
consumers), in the runtime's client (subjects derived from the credential, never named by a consumers), in the runtime's client (subjects derived from the credential, never named by a
module), and as a catalogue test asserting all 72 manifests hold no subject — because the module), and as a catalog test asserting all 72 manifests hold no subject — because the
rule held by construction, and a rule held by construction is one a later field breaks rule held by construction, and a rule held by construction is one a later field breaks
quietly. quietly.
- [x] 3.9 **seats declared by modules** — the manifest now carries `seats` (name, scope, - [x] 3.9 **seats declared by modules** — the manifest now carries `seats` (name, scope,
@@ -470,7 +470,7 @@ pays for itself furthest away.
all: a holding is derived at resolution, never stored, so nothing recorded points at an old all: a holding is derived at resolution, never stored, so nothing recorded points at an old
name (recorded as a progressive insight on ADR 0126). A **kept** rename table tells a name (recorded as a progressive insight on ADR 0126). A **kept** rename table tells a
manifest written against an old name what it became, because a module lives in its own manifest written against an old name what it became, because a module lives in its own
repository and may be registered long after the catalogue stopped using one. repository and may be registered long after the catalog stopped using one.
**A seat and the interface it delivers are different names.** The `git` seat became **A seat and the interface it delivers are different names.** The `git` seat became
`mesh-git` while the `git` *provision* it delivers did not change, and the same for the `mesh-git` while the `git` *provision* it delivers did not change, and the same for the
@@ -522,7 +522,7 @@ it, and the beds that need a mesh living on NATS can finally run.
(ADR 0129). Both sides (ADR 0129). Both sides
are behind a seam with an implementation per bus, and on the bus being built one publish does are behind a seam with an implementation per bus, and on the bus being built one publish does
what two did: the outcome is the role's own event, so the asker matches it by the id its what two did: the outcome is the role's own event, so the asker matches it by the id its
request carried, the controller records it and the catalogue places it in the graph. A builder needs a reply queue for nothing and a grant over nobody's inbox. request carried, the controller records it and the catalog places it in the graph. A builder needs a reply queue for nothing and a grant over nobody's inbox.
Checked against a running server: the round trip; a third party on the role's event hearing Checked against a running server: the round trip; a third party on the role's event hearing
the same outcome the asker did, which is what the decision rests on; work leaving the queue the same outcome the asker did, which is what the decision rests on; work leaving the queue
@@ -532,7 +532,7 @@ it, and the beds that need a mesh living on NATS can finally run.
The outcome carries the module name, because only the manifest says what was built and one The outcome carries the module name, because only the manifest says what was built and one
message now has three readers. A failed build names none: it produced no module version, and message now has three readers. A failed build names none: it produced no module version, and
the catalogue would otherwise place something that was never made. the catalog would otherwise place something that was never made.
- [x] 4.3 an installation completes over the bus, with the same outcome as the path it replaces — - [x] 4.3 an installation completes over the bus, with the same outcome as the path it replaces —
**the installer can raise it**: a foundation template that stands up the server, writes the **the installer can raise it**: a foundation template that stands up the server, writes the
server's own settings and the mesh's first user list beside them, and starts a controller server's own settings and the mesh's first user list beside them, and starts a controller
@@ -573,7 +573,7 @@ it, and the beds that need a mesh living on NATS can finally run.
correct. It uses the client a module's runtime uses, so what a person may do is answered by the correct. It uses the client a module's runtime uses, so what a person may do is answered by the
same permission list that answers it for a module and an audit has nothing separate to read. same permission list that answers it for a module and an audit has nothing separate to read.
Three decisions in it worth keeping. It lists what the **catalogue** has rather than what this Three decisions in it worth keeping. It lists what the **catalog** has rather than what this
credential may call: somebody seeing only their own tools cannot tell "not installed" from "not credential may call: somebody seeing only their own tools cannot tell "not installed" from "not
yours", and those need different people to fix them. A failed call says which of three things yours", and those need different people to fix them. A failed call says which of three things
happened — nobody serves it, this credential may not, or the tool was slow — because the happened — nobody serves it, this credential may not, or the tool was slow — because the
@@ -597,10 +597,10 @@ it, and the beds that need a mesh living on NATS can finally run.
**The catch-up half needed nothing built, and that was the answer.** It existed because a queue on **The catch-up half needed nothing built, and that was the answer.** It existed because a queue on
the bus the mesh runs on today receives only what is published after it is bound, so everything the bus the mesh runs on today receives only what is published after it is bound, so everything
built before the catalogue existed was announced to nobody — and on a fresh mesh that is always built before the catalog existed was announced to nobody — and on a fresh mesh that is always
the foundation, because those are the things the catalogue needed in order to exist the foundation, because those are the things the catalog needed in order to exist
([issue 050](../../04-ISSUES/050-the-catalogue-knows-nothing-built-before-it/00-report.md)). A ([issue 050](../../04-ISSUES/050-the-catalogue-knows-nothing-built-before-it/00-report.md)). A
whole mechanism followed: the catalogue asks, the controller re-publishes. whole mechanism followed: the catalog asks, the controller re-publishes.
A stream is a log and a consumer is a position in it. A consumer created later starts at the A stream is a log and a consumer is a position in it. A consumer created later starts at the
beginning, so the builds are simply there — asked of a running server rather than assumed, since beginning, so the builds are simply there — asked of a running server rather than assumed, since
@@ -636,8 +636,8 @@ left to be re-derived under time pressure:
| 1 | this one | prose; nothing deploys | | 1 | this one | prose; nothing deploys |
| 2 | the sdk | comments only, and no module rebuilds for it | | 2 | the sdk | comments only, and no module rebuilds for it |
| 3 | the client library | it is what a module calls to emit, and it is where the subject is derived. Until it lands, a locally-named event is published under the local name itself | | 3 | the client library | it is what a module calls to emit, and it is where the subject is derived. Until it lands, a locally-named event is published under the local name itself |
| 4 | the catalogue | every manifest and every module's code, renamed together. Safe only once the runtime derives | | 4 | the catalog | every manifest and every module's code, renamed together. Safe only once the runtime derives |
| 5 | the controller | **it refuses an old-style event name outright**, so landing it before the catalogue makes every unconverted module unregisterable | | 5 | the controller | **it refuses an old-style event name outright**, so landing it before the catalog makes every unconverted module unregisterable |
| 6 | the node-engine | last, because nothing else waits on them | | 6 | the node-engine | last, because nothing else waits on them |
Two properties make the sequence safe rather than merely ordered, and both are pinned by tests. A Two properties make the sequence safe rather than merely ordered, and both are pinned by tests. A
@@ -682,11 +682,11 @@ healthy while reacting to nothing.
- [x] 5.4 **the old broker and everything that named AMQP leave the mesh** (ADR 0131, - [x] 5.4 **the old broker and everything that named AMQP leave the mesh** (ADR 0131,
superseding ADR 0127): the two modules that superseding ADR 0127): the two modules that
required `amqp` are removed, the broker's module is unassigned and removed (**done 2026-09-28**; the predecessor's own tooling, which rode the same adopted broker, went dark with it, as ADR 0130 accepted), registration refuses required `amqp` are removed, the broker's module is unassigned and removed (**done 2026-09-28**; the predecessor's own tooling, which rode the same adopted broker, went dark with it, as ADR 0130 accepted), registration refuses
a manifest that provides or requires `amqp`, and a whole-catalogue check asserts none does. Not a manifest that provides or requires `amqp`, and a whole-catalog check asserts none does. Not
a retirement condition — a decision, taken, with the operator's "I don't care if the predecessor a retirement condition — a decision, taken, with the operator's "I don't care if the predecessor
breaks" on record (ADR 0130). breaks" on record (ADR 0130).
Retiring with it: the build outcome's second announcement under the module's own name, which Retiring with it: the build outcome's second announcement under the module's own name, which
existed only so a catalogue deployed before the rename and one after both heard it. existed only so a catalog deployed before the rename and one after both heard it.
> **The remote tooling goes with it too.** The predecessor's own mesh talks over that broker, so > **The remote tooling goes with it too.** The predecessor's own mesh talks over that broker, so
> shutting it down ends the path that reaches this installation's machines from a workstation. > shutting it down ends the path that reaches this installation's machines from a workstation.
@@ -174,7 +174,7 @@ the client/identity side and the CA are the open pieces.
`ssh-client` template can emit a `Host` block per node with the right login name. Composed `ssh-client` template can emit a `Host` block per node with the right login name. Composed
end-to-end in the controller's tests. end-to-end in the controller's tests.
**Written but not shipped:** the `ssh-client` catalogue module itself exists on a branch of the **Written but not shipped:** the `ssh-client` catalog module itself exists on a branch of the
module repository; its pull request was closed with a hold until this design is deployed, and module repository; its pull request was closed with a hold until this design is deployed, and
nothing has deployed it since. The predecessor's generator still writes every workstation's ssh nothing has deployed it since. The predecessor's generator still writes every workstation's ssh
client blocks today — which is where [issue 172](../../04-ISSUES/172-the-ssh-client-block-matches-one-spelling-of-a-machine/00-report.md) client blocks today — which is where [issue 172](../../04-ISSUES/172-the-ssh-client-block-matches-one-spelling-of-a-machine/00-report.md)
@@ -15,7 +15,7 @@ decisions:
# 30 — The mesh updates itself on a push # 30 — The mesh updates itself on a push
**Where it stands, 2026-10-17** (the records cleanup, issue 1072; playbook 07, "The design's state is written back"). Built on main: mesh-controller's delivery plans, merge gate and merge window (plan.go, release_plan.go, merge_gate.go, merge_window_bus.go, changeplan.go, with their migrations), which design 47 builds on. Not yet checked against this text nor given an as-is: issue 1075. **Where it stands, 2026-10-17** (the records cleanup, issue 1072; playbook 07, "The design's state is written back"). Built on main: mesh-controller's delivery plans, composition check and merge window (plan.go, release_plan.go, merge_gate.go, merge_window_bus.go, changeplan.go, with their migrations), which design 47 builds on. Not yet checked against this text nor given an as-is: issue 1075.
**Today the mesh does not update itself; a person drives the pipeline by hand, and one class of **Today the mesh does not update itself; a person drives the pipeline by hand, and one class of
change freezes it.** A code change lands in `mesh-controller` or `mesh-catalog`, and getting it onto change freezes it.** A code change lands in `mesh-controller` or `mesh-catalog`, and getting it onto
@@ -30,7 +30,7 @@ webhook".
seat or schema: seat or schema:
1. `module moved <module> <commit>` — tell the mesh its source advanced (the controller repo has no 1. `module moved <module> <commit>` — tell the mesh its source advanced (the controller repo has no
trigger, so this is manual; the catalogue's webhook does it automatically — see below). trigger, so this is manual; the catalog's webhook does it automatically — see below).
2. `build --behind` (or `build <repo> [--ref] [--path <subdir>]`) — the builder rebuilds and 2. `build --behind` (or `build <repo> [--ref] [--path <subdir>]`) — the builder rebuilds and
records the new image. records the new image.
3. The mesh **reconciles on its own**: the module's declaration now names the new image, the next 3. The mesh **reconciles on its own**: the module's declaration now names the new image, the next
@@ -41,7 +41,7 @@ seat or schema:
**A breaking change** — a manifest schema the controller parses differently (a fact's shape, a **A breaking change** — a manifest schema the controller parses differently (a fact's shape, a
seat's name), where the new controller cannot read the manifests the old one stored: seat's name), where the new controller cannot read the manifests the old one stored:
4. Land the code (controller + catalogue together — they are one change). 4. Land the code (controller + catalog together — they are one change).
5. Rebuild + deploy the new controller (steps 1–3). **The moment it is live it refuses the 5. Rebuild + deploy the new controller (steps 1–3). **The moment it is live it refuses the
still-old-shape stored manifests, and composition freezes for every node that runs an affected still-old-shape stored manifests, and composition freezes for every node that runs an affected
module.** Running services are untouched; only new declarations stop. module.** Running services are untouched; only new declarations stop.
@@ -63,7 +63,7 @@ automating this must solve.
### 1. The trigger today is HAL's, not the mesh's ### 1. The trigger today is HAL's, not the mesh's
Build-on-push works for the catalogue because its repository has a Gitea webhook pointing at Build-on-push works for the catalog because its repository has a Gitea webhook pointing at
`http://host.docker.internal:9877/webhook/gitea` — and **that receiver is `hal-gitea-tools.service`** `http://host.docker.internal:9877/webhook/gitea` — and **that receiver is `hal-gitea-tools.service`**
(`~/.hal/modules/hal/gitea/tools/server.js`), a *predecessor* component. The nox builder consumes (`~/.hal/modules/hal/gitea/tools/server.js`), a *predecessor* component. The nox builder consumes
build work; it does not receive Git events. So the mesh's own build pipeline currently rides on a build work; it does not receive Git events. So the mesh's own build pipeline currently rides on a
@@ -78,12 +78,12 @@ HAL service, and:
seat — `mesh-forge-trigger` or folded into the git seat's holder) that receives Git events and turns seat — `mesh-forge-trigger` or folded into the git seat's holder) that receives Git events and turns
them into build work over the broker, for **every** repository including `mesh-controller`. Replacing them into build work over the broker, for **every** repository including `mesh-controller`. Replacing
`hal-gitea-tools` is the concrete first build. Its logic already exists to copy: match the pushed `hal-gitea-tools` is the concrete first build. Its logic already exists to copy: match the pushed
repository (and changed paths, for a monorepo like the catalogue) against the build-context repository (and changed paths, for a monorepo like the catalog) against the build-context
repository of every registered module, and rebuild the matches. repository of every registered module, and rebuild the matches.
### 2. The builder validates too — and a breaking change deadlocks it ### 2. The builder validates too — and a breaking change deadlocks it
The builder embeds the same catalogue package the controller does, so **it validates a The builder embeds the same catalog package the controller does, so **it validates a
manifest against its own compiled-in seat/schema set**. A breaking change therefore couples *four* manifest against its own compiled-in seat/schema set**. A breaking change therefore couples *four*
things, not two: the controller, the **builder**, every affected manifest, and every node's host. things, not two: the controller, the **builder**, every affected manifest, and every node's host.
This session's seat rename rebuilt the controller but not the builder, and the stale builder then This session's seat rename rebuilt the controller but not the builder, and the stale builder then
@@ -126,7 +126,7 @@ automate the freeze.
Revision, ADR 0162. The Revision, ADR 0162. The
trigger exists: the forge announces a merge on the bus and the controller acts on it (ADR 0157 made trigger exists: the forge announces a merge on the bus and the controller acts on it (ADR 0157 made
the build narrate; this makes the merge a plan). A module's dependencies are one relation in the the build narrate; this makes the merge a plan). A module's dependencies are one relation in the
catalogue — `depends-on` edges of four kinds: stands-on, packages, built-by, declared. A merge takes catalog — `depends-on` edges of four kinds: stands-on, packages, built-by, declared. A merge takes
what changed and everything reachable from it along those edges, sorts the set into tiers, writes the what changed and everything reachable from it along those edges, sorts the set into tiers, writes the
plan to the store, asks the first tier and returns. Each outcome advances the plan; a tier whose plan to the store, asks the first tier and returns. Each outcome advances the plan; a tier whose
rolled-out modules a later tier is built by waits until the machines report them applied; a rolled-out modules a later tier is built by waits until the machines report them applied; a
@@ -168,7 +168,7 @@ Revision, ADR 0238
it — the module's own directory, the whole repository for a module built from its root, a repository a it — the module's own directory, the whole repository for a module built from its root, a repository a
recipe names — and a file no build reads, at the root or in a directory holding no module, rebuilds recipe names — and a file no build reads, at the root or in a directory holding no module, rebuilds
nothing. One function maps a changed file onto modules and one answers what a merge moves and builds; nothing. One function maps a changed file onto modules and one answers what a merge moves and builds;
the merge handler, the what-if, the merge gate and a pull request's check ask them. Only a commit on the the merge handler, the what-if, the composition check and a pull request's check ask them. Only a commit on the
module's trunk is registered. module's trunk is registered.
## What a push does not send (2026-10-05) ## What a push does not send (2026-10-05)
+10 -10
View File
@@ -62,7 +62,7 @@ onto this in step 3 of ADR 0116.
This is the load-bearing rule. This is the load-bearing rule.
ADR 0112 says a manifest names no node, mesh or path. A transport address is the same class of thing: if ADR 0112 says a manifest names no node, mesh or path. A transport address is the same class of thing: if
manifests held literal subjects, reorganising the subject space would mean editing every module in manifests held literal subjects, reorganising the subject space would mean editing every module in
the catalogue, and the mesh would have hundreds of copies of a decision it made once. the catalog, and the mesh would have hundreds of copies of a decision it made once.
| declared | derived | | declared | derived |
|---|---| |---|---|
@@ -94,7 +94,7 @@ lives under the emitting module's own name and the consumer carries that couplin
**How the rule is checked, because it was not.** *Added 2026-09-27, same source.* Two checks, because **How the rule is checked, because it was not.** *Added 2026-09-27, same source.* Two checks, because
the mistake happens at two scales. Per manifest, at registration: an event is a local name, and the the mistake happens at two scales. Per manifest, at registration: an event is a local name, and the
old bus's form is refused with the name to write instead. Across the whole catalogue, as a test: old bus's form is refused with the name to write instead. Across the whole catalog, as a test:
where a consumed event's emitter is present, it must emit that event. The second cannot demand a live where a consumed event's emitter is present, it must emit that event. The second cannot demand a live
emitter for everything — a module lives in its own repository and may be installed long before the emitter for everything — a module lives in its own repository and may be installed long before the
one whose events it wants — so it says nothing about an absent emitter and everything about a present one whose events it wants — so it says nothing about an absent emitter and everything about a present
@@ -117,7 +117,7 @@ caller already handles). The kind token is what makes `mesh.mod.*.event.>` a saf
first draft of this table had no token, which reads better and cannot be implemented. first draft of this table had no token, which reads better and cannot be implemented.
**The test this must pass: the manifest survives the wire changing.** Reorganise the subject space **The test this must pass: the manifest survives the wire changing.** Reorganise the subject space
and every manifest in the catalogue is still correct. That is the property and every manifest in the catalog is still correct. That is the property
ADR 0039 gave the sdk, applied to ADR 0039 gave the sdk, applied to
declarations. declarations.
@@ -172,7 +172,7 @@ can be wrong.
**And one publish reaches three audiences without anybody's inbox being opened.** A build's outcome is **And one publish reaches three audiences without anybody's inbox being opened.** A build's outcome is
the seat's own event: whoever asked matches it by the id their request carried, the controller records the seat's own event: whoever asked matches it by the id their request carried, the controller records
it, the catalogue places it in the graph. That is the fan-out a shared exchange gave for free, written it, the catalog places it in the graph. That is the fan-out a shared exchange gave for free, written
as a subject the mesh derived instead of a topology somebody configured — and it is why a holder needs as a subject the mesh derived instead of a topology somebody configured — and it is why a holder needs
no permission to publish into an asker's inbox, which is the one grant design 25 §4 refuses by name. no permission to publish into an asker's inbox, which is the one grant design 25 §4 refuses by name.
@@ -249,7 +249,7 @@ registered for every machine, sent as an event, never reached a machine assigned
consumer did not exist yet when the event passed — and a licence binding sent as events replays a consumer did not exist yet when the event passed — and a licence binding sent as events replays a
week of rotations where only the latest matters. So a module names the state it **owns** with week of rotations where only the latest matters. So a module names the state it **owns** with
`state`, and another module's it **reads** with `reads: <module>.<name>`. Each is a key-value bucket `state`, and another module's it **reads** with `reads: <module>.<name>`. Each is a key-value bucket
the controller creates from the catalogue, mesh-wide, existing from registration so a reader can the controller creates from the catalog, mesh-wide, existing from registration so a reader can
watch before the owner runs anywhere ([design 25](25-the-bus-on-nats.md) §3). Every instance of the watch before the owner runs anywhere ([design 25](25-the-bus-on-nats.md) §3). Every instance of the
owner writes; a reader reads and watches. A key may name a machine by the module's own convention; owner writes; a reader reads and watches. A key may name a machine by the module's own convention;
the mesh keeps one bucket per name, not one per machine, because "every server, for every machine" the mesh keeps one bucket per name, not one per machine, because "every server, for every machine"
@@ -326,7 +326,7 @@ Every shape above appears once, in order, and no step knows where the next one r
**A change lands.** The module holding `mesh-git` emits `pushed` — repository, ref, commit. An **A change lands.** The module holding `mesh-git` emits `pushed` — repository, ref, commit. An
event, because it is a fact about git and git's identity is the meaning. event, because it is a fact about git and git's identity is the meaning.
**The change becomes work.** The controller consumes `pushed`, asks the catalogue which modules **The change becomes work.** The controller consumes `pushed`, asks the catalog which modules
are built from that repository and path are built from that repository and path
(ADR 0069), and submits one (ADR 0069), and submits one
**job** per affected module to the `mesh-build-machine` seat. The builder stays simple: it builds **job** per affected module to the `mesh-build-machine` seat. The builder stays simple: it builds
@@ -366,7 +366,7 @@ not to an address it was given at genesis. Held and retried while the store rest
chain above went dark at the moment it touched a machine: nothing said which version a machine now runs, chain above went dark at the moment it touched a machine: nothing said which version a machine now runs,
or that it refused to. The controller states those as facts under its own seat's namespace, when what or that it refused to. The controller states those as facts under its own seat's namespace, when what
a machine runs changes rather than on every convergence pass, and anything that cares subscribes the way a machine runs changes rather than on every convergence pass, and anything that cares subscribes the way
the catalogue subscribes to `built` (ADR 0134). the catalog subscribes to `built` (ADR 0134).
The facts are second-hand by design — one emitter, one ordering — and a machine that cannot reach the bus The facts are second-hand by design — one emitter, one ordering — and a machine that cannot reach the bus
produces none, so absence is not health. produces none, so absence is not health.
@@ -482,7 +482,7 @@ addresses will believe a class of bug is fixed when it is untouched.
controller's address — recorded somewhere, at some moment, and wrong as soon as anything moved. controller's address — recorded somewhere, at some moment, and wrong as soon as anything moved.
Now it publishes to `mesh.seat.mesh-controller.report` and the bus routes it to whoever holds the Now it publishes to `mesh.seat.mesh-controller.report` and the bus routes it to whoever holds the
seat. Nothing anywhere records where the controller is, so nothing can record it *wrongly*. The seat. Nothing anywhere records where the controller is, so nothing can record it *wrongly*. The
same is true of the builder, the catalogue, the telegram sender. This class is not mitigated; it same is true of the builder, the catalog, the telegram sender. This class is not mitigated; it
is gone, because the information is no longer stored. is gone, because the information is no longer stored.
**Something that is not on the bus: the address stays, exactly as before.** A module that requires **Something that is not on the bus: the address stays, exactly as before.** A module that requires
@@ -580,11 +580,11 @@ billing existing under that name.
## 12. How it is checked ## 12. How it is checked
- **A manifest holds no subject.** A catalogue test: no manifest contains a string matching the - **A manifest holds no subject.** A catalog test: no manifest contains a string matching the
subject grammar. The rule is worthless if it is followed by convention. subject grammar. The rule is worthless if it is followed by convention.
- **A preparation is given what the module is given.** A composition test: what the preparation - **A preparation is given what the module is given.** A composition test: what the preparation
entrypoint receives equals what the module's own code receives, asserted rather than written twice — entrypoint receives equals what the module's own code receives, asserted rather than written twice —
which is the drift a hand-written step invites, three times over in the catalogue today. which is the drift a hand-written step invites, three times over in the catalog today.
- **A convergence that changed nothing says nothing.** Two identical reports, one emitted fact: what is - **A convergence that changed nothing says nothing.** Two identical reports, one emitted fact: what is
guarded against is a fact per minute per machine, which is a stream nobody reads. guarded against is a fact per minute per machine, which is a stream nobody reads.
- **Permissions are exactly the three namespaces.** A composition test per module: the derived - **Permissions are exactly the three namespaces.** A composition test per module: the derived
@@ -135,7 +135,7 @@ by side until nothing is bound to the old one.
*Amended 2026-10-07 (ADR 0246, *Amended 2026-10-07 (ADR 0246,
[issue 298](../../04-ISSUES/298-a-new-seat-verb-deadlocks-across-two-repositories/00-report.md)):* [issue 298](../../04-ISSUES/298-a-new-seat-verb-deadlocks-across-two-repositories/00-report.md)):*
**additive takes two steps when the seat and its holder are in different repositories.** That is the **additive takes two steps when the seat and its holder are in different repositories.** That is the
case for every mesh seat whose holder is a catalogue module. §3 makes every promised verb a condition of case for every mesh seat whose holder is a catalog module. §3 makes every promised verb a condition of
holding, and a claim may name no verb the seat does not promise. So a verb added to the seat in the holding, and a claim may name no verb the seat does not promise. So a verb added to the seat in the
controller refuses today's holder, and a holder that serves it is refused by today's controller. Neither controller refuses today's holder, and a holder that serves it is refused by today's controller. Neither
change can land first. An added verb therefore lands in three steps, each its own pull request: change can land first. An added verb therefore lands in three steps, each its own pull request:
@@ -163,7 +163,7 @@ still a new version.
of the subject table. of the subject table.
- **An added verb lands in steps, and each step is checked (§7).** The controller's claim check refuses - **An added verb lands in steps, and each step is checked (§7).** The controller's claim check refuses
a claim that serves a verb the seat does not promise, and a holder that misses a required verb. It runs a claim that serves a verb the seat does not promise, and a holder that misses a required verb. It runs
at registration, at handover, and in `module check`, which the catalogue's merge check runs over every at registration, at handover, and in `module check`, which the catalog's merge check runs over every
manifest with the controller the mesh runs. So a step out of order fails the pull request that took manifest with the controller the mesh runs. So a step out of order fails the pull request that took
it. That an optional verb stays optional when read back from the store is held by mesh-controller's it. That an optional verb stays optional when read back from the store is held by mesh-controller's
`TestAnOptionalVerbStaysOptionalInASetReadFromTheStore` (#114), and that a seat's row gains a verb's `TestAnOptionalVerbStaysOptionalInASetReadFromTheStore` (#114), and that a seat's row gains a verb's
+5 -5
View File
@@ -28,7 +28,7 @@ it put it there (ADR 0152).
## 1. What it is ## 1. What it is
A module, `mesh-console`, in the catalogue. Its image is the tool runner's own — the client that A module, `mesh-console`, in the catalog. Its image is the tool runner's own — the client that
already speaks the bus as a command line and as an MCP server — started in a mode that reads the already speaks the bus as a command line and as an MCP server — started in a mode that reads the
module's credential and listens on loopback. It has no state, no provision, no seat. What it needs is module's credential and listens on loopback. It has no state, no provision, no seat. What it needs is
the bus, which it gets the way every module does: a credential the mesh minted for `<node>.mesh-console`, the bus, which it gets the way every module does: a credential the mesh minted for `<node>.mesh-console`,
@@ -42,7 +42,7 @@ consumer is the operator's agent, [36 — The operator's agent on a machine](36-
a machine without the mesh MCP server refuses such a module by name. Nothing else above changes: no seat, no a machine without the mesh MCP server refuses such a module by name. Nothing else above changes: no seat, no
state, no tools of its own. state, no tools of its own.
Its manifest says three things nothing else in the catalogue says together: Its manifest says three things nothing else in the catalog says together:
- `invokes: ["*"]` — it calls every tool on the mesh, and the bus grants exactly that publish side; - `invokes: ["*"]` — it calls every tool on the mesh, and the bus grants exactly that publish side;
- `listens` on a port `from: machine` — the filter opens nothing for it, because loopback is not outside; - `listens` on a port `from: machine` — the filter opens nothing for it, because loopback is not outside;
@@ -74,7 +74,7 @@ its own under that module's name, `mesh.mod.<module>.tool.tools`, answering the
descriptions and argument schemas — the definitions from the code that answers them, and from nowhere descriptions and argument schemas — the definitions from the code that answers them, and from nowhere
else. A module may not name a tool of its own `tools`; the runtime refuses the collision at load. else. A module may not name a tool of its own `tools`; the runtime refuses the collision at load.
**The mesh MCP server asks the catalogue which modules the mesh holds, then asks each.** `catalog_modules` **The mesh MCP server asks the catalog which modules the mesh holds, then asks each.** `catalog_modules`
answers the roster; one `tools` request per module, in parallel, answers the list. The bus refuses at answers the roster; one `tools` request per module, in parallel, answers the list. The bus refuses at
once a request nothing serves, so a module that is not running costs nothing and is named in the answer once a request nothing serves, so a module that is not running costs nothing and is named in the answer
as not answering, rather than silently absent — *silence and success must never look alike*. The list is as not answering, rather than silently absent — *silence and success must never look alike*. The list is
@@ -106,7 +106,7 @@ answers is reached through them by its address: `<seat>.<verb>` for a seat held
`<node>/<seat>.<verb>` for one held per machine, `<node>/<module>.<tool>` for an assignment, and `<node>/<seat>.<verb>` for one held per machine, `<node>/<module>.<tool>` for an assignment, and
`<module>.<tool>` as well for a module whose instances are interchangeable. A module that is not `<module>.<tool>` as well for a module whose instances are interchangeable. A module that is not
interchangeable is called with its machine or refused with the machines it runs on. Each discovery interchangeable is called with its machine or refused with the machines it runs on. Each discovery
verb asks the mesh when it is called, so nothing is kept for a session's length; the flat catalogue verb asks the mesh when it is called, so nothing is kept for a session's length; the flat catalog
stays reachable through the `mesh` client and a setting, unannounced. stays reachable through the `mesh` client and a setting, unannounced.
**What exists is what announced itself** (ADR 0197): **What exists is what announced itself** (ADR 0197):
@@ -159,7 +159,7 @@ machine with nothing else on it does; the mesh MCP server binds whatever it is g
Two things shipped bent, both stated in [`00-as-is/13-the-console.md`](../00-as-is/13-the-console.md): Two things shipped bent, both stated in [`00-as-is/13-the-console.md`](../00-as-is/13-the-console.md):
the person's client through the mesh MCP server (`--console`) exists and was exercised in the test suite, not the person's client through the mesh MCP server (`--console`) exists and was exercised in the test suite, not
on the live mesh; and a module registered from the catalogue by hand recorded its source as a URL rather on the live mesh; and a module registered from the catalog by hand recorded its source as a URL rather
than as a path on the git seat, because `--self` takes the forge path form — the rebuild-on-merge still than as a path on the git seat, because `--self` takes the forge path form — the rebuild-on-merge still
matched it by URL. matched it by URL.
@@ -132,7 +132,7 @@ a hypothesis (ADR 0153);
the mesh is asked and changed through the controller seat's verbs; the forge through the forge module's the mesh is asked and changed through the controller seat's verbs; the forge through the forge module's
tools; a licence through the `anthropic-licence-manager` seat's verbs, never by editing a file. The hard tools; a licence through the `anthropic-licence-manager` seat's verbs, never by editing a file. The hard
rules in new words: a file the mesh manages is changed through the verb that owns it or through the rules in new words: a file the mesh manages is changed through the verb that owns it or through the
catalogue, never on disk; a store's database is never written by hand; main is never pushed; the mesh catalog, never on disk; a store's database is never written by hand; main is never pushed; the mesh
creates no symlinks and nobody else does; a package is declared, not installed by hand. The glossary's creates no symlinks and nobody else does; a package is declared, not installed by hand. The glossary's
words, none of the predecessor's. words, none of the predecessor's.
@@ -157,6 +157,16 @@ does the job, or with the rule that a missing tool is created in its owning modu
managed directory, so a session cannot change it, judges with the machines and replaced commands the module managed directory, so a session cannot change it, judges with the machines and replaced commands the module
last asked of the controller, and records every refusal and every operator's override in the module's state. last asked of the controller, and records every refusal and every operator's override in the module's state.
**The wait list and the Stop hook** (ADR 0489, ADR 0490). Beside the guard the plugin carries two more of the
module's own commands as hooks. At every session start the session-start hook prints the session's id and every
open wait on the machine by the name it is kept under, with the call that takes a name back after a /clear (which
gives a session a new id). At every stop the Stop hook reads the session's waits, which `claude_code_wait` keeps
in the module's state under the session's name (who@node) and mirrors into the agent's home, and refuses the stop
while a wait is within its bound and nothing follows it, or past its bound and not yet told to the operator; it
lets the stop through silently when there are no waits, says so on the screen when the list cannot be read, and
refuses the same stop at most three times. Like the guard, both hooks ship in the plugin only: the hook
registration tools refuse a hook at mesh or node scope (issue 340).
## 4. The mesh MCP server ## 4. The mesh MCP server
> **Revised 2026-10-03, building it.** The vendor's managed-settings key for tool servers refuses any > **Revised 2026-10-03, building it.** The vendor's managed-settings key for tool servers refuses any
@@ -221,7 +231,7 @@ All four nodes carry one since 2026-10-03. **Per node:** the role. **Per mesh or
extra tool servers, and the operator's managed-settings keys (§2). The controller's verb replaces a extra tool servers, and the operator's managed-settings keys (§2). The controller's verb replaces a
setting layer whole, so a layer set for one of these keeps the others it already held. **Prerequisite:** the manager holds its seat and has adopted the licences. setting layer whole, so a layer set for one of these keeps the others it already held. **Prerequisite:** the manager holds its seat and has adopted the licences.
**Order:** the manager assigned and a refresh observed; the mesh MCP server's provision in the catalogue; this **Order:** the manager assigned and a refresh observed; the mesh MCP server's provision in the catalog; this
module on one workstation; the six predecessor files and the hand-made mesh MCP server entry removed there; a module on one workstation; the six predecessor files and the hand-made mesh MCP server entry removed there; a
new session read to confirm it sees the mesh's instruction file, the mesh MCP server's five tools under `mesh`, and new session read to confirm it sees the mesh's instruction file, the mesh MCP server's five tools under `mesh`, and
its licence; then the rest. its licence; then the rest.
@@ -405,6 +415,7 @@ the helper of §9 where that is an agent account):
| `rules/<name>.md` | one personal rule each | | `rules/<name>.md` | one personal rule each |
| `skills/<name>/`, `commands/<name>.md`, `agents/<name>.md` | the operator's own skills, commands and subagents at the operator scope, under their own names; and the home-scope items where the home is an agent account's own | | `skills/<name>/`, `commands/<name>.md`, `agents/<name>.md` | the operator's own skills, commands and subagents at the operator scope, under their own names; and the home-scope items where the home is an agent account's own |
| `hooks/` | nothing: a hook is the terminal's, in the managed settings. The directory is kept empty | | `hooks/` | nothing: a hook is the terminal's, in the managed settings. The directory is kept empty |
| `nox-mesh/waits/<session-id>.json`, `nox-mesh/waits/_index.json` | the module's own mirror (ADR 0489, ADR 0490): per Claude Code session id, the open waits of every name bound to that id; and the machine's open names, written whole by `claude_code_wait` and read by the plugin's session-start and Stop hooks; rebuilt from the module's state, never a person's |
| `output-styles/<name>.md`, `keybindings.json` | the operator's output styles and keybindings at the operator scope | | `output-styles/<name>.md`, `keybindings.json` | the operator's output styles and keybindings at the operator scope |
| `settings.json` | the operator's registered settings, preference keys only, with the keys the program itself writes on a person's choice (the model, the spinner, the drafts, the other preferences the vendor's own commands set) taken over into the registration when the program changes them, so a choice made in one session follows the operator. A key that decides what runs (`hooks`, `permissions`, `mcpServers`, `env`, `apiKeyHelper`, `statusLine`, `autoMode`, any key not on the list) is never taken over: changed on disk, it is written back on the look that finds it. `autoMode` is a permission rule, so it lives in the managed settings at the terminal | | `settings.json` | the operator's registered settings, preference keys only, with the keys the program itself writes on a person's choice (the model, the spinner, the drafts, the other preferences the vendor's own commands set) taken over into the registration when the program changes them, so a choice made in one session follows the operator. A key that decides what runs (`hooks`, `permissions`, `mcpServers`, `env`, `apiKeyHelper`, `statusLine`, `autoMode`, any key not on the list) is never taken over: changed on disk, it is written back on the look that finds it. `autoMode` is a permission rule, so it lives in the managed settings at the terminal |
@@ -19,7 +19,7 @@ decisions:
# 37 — The operator's machine # 37 — The operator's machine
**Every configurable thing on a node is a module, the home included, and the same catalogue serves **Every configurable thing on a node is a module, the home included, and the same catalog serves
a server and a laptop.** One default configuration per module, varied per node by a setting or a a server and a laptop.** One default configuration per module, varied per node by a setting or a
kept region; roles a machine has once as node-scoped seats with tool contracts; one tool runner kept region; roles a machine has once as node-scoped seats with tool contracts; one tool runner
per node serving every module's tools outside any container per node serving every module's tools outside any container
@@ -117,7 +117,7 @@ resolver is asked by the second graphical module; the display server itself is g
all four today. Nothing home-scoped composes before it. all four today. Nothing home-scoped composes before it.
2. **The tool runner** — `mesh-host` supervises it; mesh-tools serves bundles from memberships 2. **The tool runner** — `mesh-host` supervises it; mesh-tools serves bundles from memberships
and reloads; mesh-controller composes the bundle into the declaration and the memberships to one and reloads; mesh-controller composes the bundle into the declaration and the memberships to one
runtime per node; the catalogue renames the mesh MCP server. Proven when the packet-filter verbs answer runtime per node; the catalog renames the mesh MCP server. Proven when the packet-filter verbs answer
from it and its container is gone. from it and its container is gone.
3. **`zsh`**, the first environment module: seat, `user` shape, home files, `execute`. Proven on a 3. **`zsh`**, the first environment module: seat, `user` shape, home files, `execute`. Proven on a
server first, then every node. server first, then every node.
@@ -52,7 +52,7 @@ as design 28's: nothing here is new ground; every package reshapes something sta
| the controller's bus principals | Go: one principal per module per node, grants from what it declares | 132 lines | gains one principal per node for the runtime | | the controller's bus principals | Go: one principal per module per node, grants from what it declares | 132 lines | gains one principal per node for the runtime |
| the controller's memberships | Go: one per assignment, the subjects a runtime serves | 143 lines | **unchanged** in shape; the runtime reads several | | the controller's memberships | Go: one per assignment, the subjects a runtime serves | 143 lines | **unchanged** in shape; the runtime reads several |
| the controller's declaration composer | Go, one file | 2 053 lines | gains the runtime's process, the bundles' archives, two env words | | the controller's declaration composer | Go, one file | 2 053 lines | gains the runtime's process, the bundles' archives, two env words |
| the catalogue | 35 manifests build a per-module tool container on the runtime's base image | — | none do; the runtime is a module of its own | | the catalog | 35 manifests build a per-module tool container on the runtime's base image | — | none do; the runtime is a module of its own |
**Two measurements decide the shape.** The node-engine needs no change: a `process` and an `archive` are **Two measurements decide the shape.** The node-engine needs no change: a `process` and an `archive` are
what the runtime and a bundle are, and both are applied today. And the runtime already does what the runtime and a bundle are, and both are applied today. And the runtime already does
@@ -140,11 +140,11 @@ with the shortcut off answers the same.
a module whose own code is an image artifact is refused, whatever image it is built on. a module whose own code is an image artifact is refused, whatever image it is built on.
*Amended 2026-10-02, at WP3.* The gate refuses the pattern **spreading**, not standing: a module *Amended 2026-10-02, at WP3.* The gate refuses the pattern **spreading**, not standing: a module
new to the catalogue in that shape, or one that had already moved to a bundle and returns to it, new to the catalog in that shape, or one that had already moved to a bundle and returns to it,
is refused; a module the catalogue already holds in that shape — judged from the manifest it is refused; a module the catalog already holds in that shape — judged from the manifest it
holds and what that module's newest build stood on — is rebuilt without complaint. The day the holds and what that module's newest build stood on — is rebuilt without complaint. The day the
runtime arrives some thirty such modules stand, each moves in its own change from WP4 on, and a runtime arrives some thirty such modules stand, each moves in its own change from WP4 on, and a
gate refusing every rebuild in the meantime would stop the catalogue's pipeline to make a point gate refusing every rebuild in the meantime would stop the catalog's pipeline to make a point
this record already makes. this record already makes.
**Proof.** Composition tests: a node with three assigned modules, one holding a seat, yields one **Proof.** Composition tests: a node with three assigned modules, one holding a seat, yields one
@@ -159,7 +159,7 @@ the runtime is registered.
**What changes.** mesh-tools gains a `bundle` artifact of itself beside its images, and its manifest **What changes.** mesh-tools gains a `bundle` artifact of itself beside its images, and its manifest
becomes the `node-tools` module: a package for the interpreter, the loopback listener the mesh MCP server becomes the `node-tools` module: a package for the interpreter, the loopback listener the mesh MCP server
declared, `invokes: *`, and nothing else — the process is the controller's to compose (WP2). In the declared, `invokes: *`, and nothing else — the process is the controller's to compose (WP2). In the
catalogue, `mesh-console` is retired as a module and `node-tools` assigned where it was. The catalog, `mesh-console` is retired as a module and `node-tools` assigned where it was. The
runtime's `serve` keeps answering MCP on loopback; the person's end of it keeps the name "console" runtime's `serve` keeps answering MCP on loopback; the person's end of it keeps the name "console"
([glossary](../../00-META/glossary.md)). ([glossary](../../00-META/glossary.md)).
@@ -178,7 +178,7 @@ runtime accepts. *Built 2026-10-02* (mesh-tools `c46f950`, mesh-controller `ca7e
`729a5f9`). *Proven live 2026-10-02/03, on all four machines*: the mesh MCP server's container is gone, `729a5f9`). *Proven live 2026-10-02/03, on all four machines*: the mesh MCP server's container is gone,
`node-tools` runs as a unit the node-engine wrote, as the operator's account, `tools/list` on each loopback `node-tools` runs as a unit the node-engine wrote, as the operator's account, `tools/list` on each loopback
answers with the same 219 tools as before, and the controller's verbs answer through it; `mesh-console` answers with the same 219 tools as before, and the controller's verbs answer through it; `mesh-console`
retired from the catalogue. Three things the step found are issues retired from the catalog. Three things the step found are issues
[203](../../04-ISSUES/203-a-fresh-assignment-is-pushed-before-its-credential-exists/00-report.md), [203](../../04-ISSUES/203-a-fresh-assignment-is-pushed-before-its-credential-exists/00-report.md),
[204](../../04-ISSUES/204-a-controller-handover-re-sent-every-node-a-stale-declaration/00-report.md) and [204](../../04-ISSUES/204-a-controller-handover-re-sent-every-node-a-stale-declaration/00-report.md) and
[205](../../04-ISSUES/205-a-package-resource-fails-against-a-stale-package-database/00-report.md). [205](../../04-ISSUES/205-a-package-resource-fails-against-a-stale-package-database/00-report.md).
@@ -224,7 +224,7 @@ the node-engine re-creates the runtime's process on every reconcile (resolved th
credential and state directory gone, the client through `sudo` since the daemon's socket is root's; credential and state directory gone, the client through `sudo` since the daemon's socket is root's;
proven on all four machines — `status`, `banned` and the module's own `fail2ban_settings` answer from proven on all four machines — `status`, `banned` and the module's own `fail2ban_settings` answer from
the runtime, no `mesh-fail2ban` container, the runtime serving both bundles. Two holders moved; of the the runtime, no `mesh-fail2ban` container, the runtime serving both bundles. Two holders moved; of the
thirty-three tool containers the catalogue held, thirty-one remain, and all but these two carried their thirty-three tool containers the catalog held, thirty-one remain, and all but these two carried their
module's configuration and secrets in the container's environment, which a bundle does not have — the module's configuration and secrets in the container's environment, which a bundle does not have — the
question research research 020 opened question research research 020 opened
and ADR 0192 and ADR 0192
@@ -235,7 +235,7 @@ container's, the runtime hands each bundle its own. That is WP4b below.
*mesh-controller, mesh-tools, mesh-catalog. One day. The rest of ADR 0175, under ADR 0192.* *mesh-controller, mesh-tools, mesh-catalog. One day. The rest of ADR 0175, under ADR 0192.*
**What changes**, in order: the manifest's tools artifact gains `env` and the catalogue check **What changes**, in order: the manifest's tools artifact gains `env` and the catalog check
refuses a secret's content in it; the composer resolves a bundle's `env` per machine and carries it refuses a secret's content in it; the composer resolves a bundle's `env` per machine and carries it
beside the bundle's archive, `restart-on` included; the runtime hands each bundle its own beside the bundle's archive, `restart-on` included; the runtime hands each bundle its own
environment — the contributor's argument for an imported bundle, the child's environment for a environment — the contributor's argument for an imported bundle, the child's environment for a
@@ -1,9 +1,10 @@
--- ---
layer: to-be layer: to-be
status: implemented status: in-progress
code: [mesh-catalog modules/claude-licence-manager] code: [mesh-catalog modules/claude-licence-manager, mesh-vault]
updated: 2026-10-04 updated: 2026-10-26
decisions: decisions:
- "ADR 0550"
- "ADR 0209" - "ADR 0209"
- "ADR 0206" - "ADR 0206"
- "ADR 0183" - "ADR 0183"
@@ -18,13 +19,35 @@ decisions:
# 39 — The Anthropic licence manager # 39 — The Anthropic licence manager
**One module knows every Anthropic licence the mesh has, keeps each alive, decides which consumer gets **One module knows every Anthropic licence the mesh has, keeps each alive, and decides which consumer gets
which, and hands every node's agent its token over the bus.** which; the vault keeps every grant and token and hands every node's agent its token over the bus**
(ADR 0550 §3, amending the first shape, in which the manager kept the grants and handed the tokens itself).
ADR 0183 ADR 0183
decides it; this is the shape. It is the successor of the predecessor's manager module, built from what decides it; this is the shape. It is the successor of the predecessor's manager module, built from what
that module learned the hard way, and the counterpart of [36 — The operator's agent on a machine](36-the-operators-agent-on-a-machine.md), that module learned the hard way, and the counterpart of [36 — The operator's agent on a machine](36-the-operators-agent-on-a-machine.md),
which is the consumer on every node. which is the consumer on every node.
## Every grant in the vault
*Decided 2026-10-26 (ADR 0550 §3), on the operator's answer that every secret lives in the vault; built in
ADR 0547's second milestone (issue 1126). Until it is delivered, the sections below hold as first written.*
- **The vault keeps every licence's secrets**: the refresh tokens, the API key and the access tokens are vault
secrets ([24](24-the-secrets-vault.md)). The manager keeps its bindings, its licence records without a secret
(account, kind, health, usage, audit), and nothing at rest that is secret.
- **The manager stays the one rotation source.** It alone calls the vendor's token endpoint: to refresh or adopt,
it asks the vault for the grant sealed to itself and hands what it gets back to the vault. A grant offered at
login, sealed to the manager's key (its `public-key` verb is kept), is handed on to the vault the same way.
- **A holder's token comes from the vault.** The agent module on a node registers its public half with the vault,
not the seat, and asks the vault for its token sealed to it. The vault serves only the licence the manager's
binding names for that holder, and says `secret-changed`, never a value. The seat's `current` verb answers which
licence a consumer holds and its health, never the token.
- **It waits for accounts of their own.** The vault hands a value only to a module running under a bus account of
its own (issue 569), so the move waits until the manager and each agent module run under one.
*How it is checked:* a unit test that the vault refuses a token to a holder no binding names or to one without an
account of its own, and a scan after the move that the manager's tables hold no grant.
## 1. What it is ## 1. What it is
A module, `claude-licence-manager`, holding the mesh-scoped seat **`anthropic-licence-manager`**. One A module, `claude-licence-manager`, holding the mesh-scoped seat **`anthropic-licence-manager`**. One
@@ -43,7 +66,8 @@ Its store holds four things:
**The grants are encrypted with a key the vault made for the manager** — its one `secret` requirement. **The grants are encrypted with a key the vault made for the manager** — its one `secret` requirement.
The vault keeps that key; the manager keeps the grants. That is ADR 0050's carve-out, one module, one The vault keeps that key; the manager keeps the grants. That is ADR 0050's carve-out, one module, one
node, the long-lived grants only. node, the long-lived grants only. ADR 0550 §3 ends the carve-out: the grants move into the vault, and the
`licences` table keeps no grant (*Every grant in the vault*, above).
## 2. The licences it manages today ## 2. The licences it manages today
@@ -77,7 +101,9 @@ Carried from the predecessor, where each rule was earned by an incident:
## 4. Handing a token to a node ## 4. Handing a token to a node
*Amended 2026-10-04 by ADR 0206*, replacing the manager's visits: **what each consumer *Amended 2026-10-04 by ADR 0206*, replacing the manager's visits: **what each consumer
should hold is the manager's state, and the token is fetched when it changes.** should hold is the manager's state, and the token is fetched when it changes.** *Amended 2026-10-26 by
ADR 0550 §3:* the token is fetched from the vault, not the seat; the `bindings` state and its generation stay
the manager's, and the vault serves the licence a binding names.
- **The manager keeps a `bindings` state**, one key per consumer: the licence, its kind, and a - **The manager keeps a `bindings` state**, one key per consumer: the licence, its kind, and a
**generation** that increases with every rotation and every switch. Nothing in it is secret. **generation** that increases with every rotation and every switch. Nothing in it is secret.
@@ -162,7 +188,8 @@ gone; a rotation or a switch is a new generation in the `bindings` state.
identity, expiry, failures, who is bound), `bindings`, `bind`, `switch`, `release`, `refresh` (now, one identity, expiry, failures, who is bound), `bindings`, `bind`, `switch`, `release`, `refresh` (now, one
or all), `usage` (current and history), `adopt` (a file on the manager's node, or a key sealed to its or all), `usage` (current and history), `adopt` (a file on the manager's node, or a key sealed to its
`public-key` — ADR 0209), `public-key`, and `current` (a consumer's token, sealed to the key the `public-key` — ADR 0209), `public-key`, and `current` (a consumer's token, sealed to the key the
consumer sends — ADR 0206). The manager asks a node for a candidate grant by the agent module's own tool. consumer sends — ADR 0206; from ADR 0550 §3, which licence a consumer holds and its health, never the
token, which the vault hands). The manager asks a node for a candidate grant by the agent module's own tool.
## 8. Settings ## 8. Settings
@@ -179,6 +206,8 @@ one definition serves and one mesh may differ.
| a grant offered with a mismatching identity is refused and one notification emitted | ADR 0183, attribution | | a grant offered with a mismatching identity is refused and one notification emitted | ADR 0183, attribution |
| a failing licence notifies once, and once a day after, not once per tick | §3 | | a failing licence notifies once, and once a day after, not once per tick | §3 |
| the mesh MCP server lists the seat's verbs and `switch` changes a workstation's token end to end | ADR 0132, the exit of the build | | the mesh MCP server lists the seat's verbs and `switch` changes a workstation's token end to end | ADR 0132, the exit of the build |
| the vault refuses a token to a holder no binding names, or to one without a bus account of its own | ADR 0550 §3 |
| after the move, the manager's tables hold no grant | ADR 0550 §3 |
## What this does not settle ## What this does not settle
@@ -60,8 +60,8 @@ Measured 2026-10-03 on the four machines and in the repositories.
| a bundle's words | paths and constants written with `${dir:…}` and `${port:…}` only; a fact the mesh knows reaches a bundle as a file whose path is a word | the agent module's facts file and settings file | | a bundle's words | paths and constants written with `${dir:…}` and `${port:…}` only; a fact the mesh knows reaches a bundle as a file whose path is a word | the agent module's facts file and settings file |
| a bundle calling a tool | `mesh/ask` through the runtime that launched it (ADR 0198); no bundle holds a bus credential | how the manager visits every node | | a bundle calling a tool | `mesh/ask` through the runtime that launched it (ADR 0198); no bundle holds a bus credential | how the manager visits every node |
| a module's own long-running code | a bundle the runtime launches and restarts (ADR 0198); the runtime's subscription and grants built, the modules moving in design 38 WP4c's waves | the manager's daemon (WP3, WP4) | | a module's own long-running code | a bundle the runtime launches and restarts (ADR 0198); the runtime's subscription and grants built, the modules moving in design 38 WP4c's waves | the manager's daemon (WP3, WP4) |
| the vendor's refresh, the sealed box, the grant file | `anthropic-manager` in the catalogue, built on the controller placement ADR 0183 moved away from; assigned to nothing | its client ported into the manager; the module retired (WP6) | | the vendor's refresh, the sealed box, the grant file | `anthropic-manager` in the catalog, built on the controller placement ADR 0183 moved away from; assigned to nothing | its client ported into the manager; the module retired (WP6) |
| the credentials write, the strip, the identity read | `anthropic-consumer` in the catalogue; tested; assigned to nothing | ported into the agent module with its tests; the module retired (WP6) | | the credentials write, the strip, the identity read | `anthropic-consumer` in the catalog; tested; assigned to nothing | ported into the agent module with its tests; the module retired (WP6) |
| the predecessor's manager and consumer | the lease per licence, the expiry floor, the lineage comparison, the identity guard, three touchpoints, cooldowns | ported as logic with its tests | | the predecessor's manager and consumer | the lease per licence, the expiry floor, the lineage comparison, the identity guard, three touchpoints, cooldowns | ported as logic with its tests |
## The order the work allows ## The order the work allows
@@ -99,7 +99,7 @@ resolves it. The runtime's port stays what its code fixes; assigning it is
second question and not this package's. second question and not this package's.
**Proof.** The plan for a workstation carrying a consumer of `mcp-endpoint` shows it bound to the **Proof.** The plan for a workstation carrying a consumer of `mcp-endpoint` shows it bound to the
runtime's port; the controller's tests and the catalogue's checks pass. runtime's port; the controller's tests and the catalog's checks pass.
## WP2 — The agent module ## WP2 — The agent module
@@ -130,7 +130,7 @@ runtime's port; the controller's tests and the catalogue's checks pass.
**Proof, before anything runs live.** Unit tests: the renderer writes the mesh's keys and nothing else; **Proof, before anything runs live.** Unit tests: the renderer writes the mesh's keys and nothing else;
it writes nothing when nothing changed; the credentials write strips a refresh token and is atomic; the it writes nothing when nothing changed; the credentials write strips a refresh token and is atomic; the
lineage cases from the predecessor; a sealed hand-over opens only with the module's key; no tool's answer lineage cases from the predecessor; a sealed hand-over opens only with the module's key; no tool's answer
contains a token. The catalogue's checks pass. contains a token. The catalog's checks pass.
**Proof, live, on one workstation, configuration only.** Assign the module; set the node's role; push. **Proof, live, on one workstation, configuration only.** Assign the module; set the node's role; push.
The agent's managed directory holds the two files; everything under the person's agent directory is The agent's managed directory holds the two files; everything under the person's agent directory is
@@ -188,11 +188,11 @@ credentials file is left as it was and the agent authenticates through the key-h
*The live mesh and mesh-catalog. One day.* The module on every node, the predecessor's files removed on *The live mesh and mesh-catalog. One day.* The module on every node, the predecessor's files removed on
the second workstation; a login under a licence's account collected and adopted, and one under the wrong the second workstation; a login under a licence's account collected and adopted, and one under the wrong
account refused and notified; `anthropic-manager` and `anthropic-consumer` retired from the catalogue; account refused and notified; `anthropic-manager` and `anthropic-consumer` retired from the catalog;
designs 36 and 39 set to `implemented` with the as-is written designs 36 and 39 set to `implemented` with the as-is written
([playbook 02](../../00-META/process/02-graduation.md)). ([playbook 02](../../00-META/process/02-graduation.md)).
**Proof.** `claude_code_status` answers on every node; the refusal's notification arrived; the catalogue **Proof.** `claude_code_status` answers on every node; the refusal's notification arrived; the catalog
has no module built on the old placement. has no module built on the old placement.
## The home, personal rules and lessons — WP7 to WP10 ## The home, personal rules and lessons — WP7 to WP10
@@ -259,7 +259,7 @@ WP10 the rest of the nodes, and the as-is (the live mesh)
neither list, the watched entries and the pending requests. neither list, the watched entries and the pending requests.
**Proof, before anything runs live.** The module's tests of design 36 "How it is checked" for ADR 0273 §1, **Proof, before anything runs live.** The module's tests of design 36 "How it is checked" for ADR 0273 §1,
§2, §3 and §4, each failing on the commit before. The catalogue's checks pass. §2, §3 and §4, each failing on the commit before. The catalog's checks pass.
### WP8 — Personal rules and the operator's items, live ### WP8 — Personal rules and the operator's items, live
@@ -146,7 +146,7 @@ against WP2's controller before anything is published.
the node. The rendering is ADR 0203's and ADR 0204's: module order, a naming line per contribution, the node. The rendering is ADR 0203's and ADR 0204's: module order, a naming line per contribution,
`PATH` entries added only when missing. `${machine:…}` in a contributed value is resolved first. `PATH` entries added only when missing. `${machine:…}` in a contributed value is resolved first.
- **Refusals.** A variable set by two modules on one node is refused, naming both. A placeholder in a - **Refusals.** A variable set by two modules on one node is refused, naming both. A placeholder in a
module that does not claim the matching seat is refused, both at the catalogue check and at module that does not claim the matching seat is refused, both at the catalog check and at
composition. composition.
**Proof.** The controller's tests: **Proof.** The controller's tests:
@@ -157,7 +157,7 @@ against WP2's controller before anything is published.
- each refusal, by name; - each refusal, by name;
- the seat table carries both seats and refuses a module declaring either. - the seat table carries both seats and refuses a module declaring either.
The catalogue check over the whole catalogue passes. The catalog check over the whole catalog passes.
## WP3 — The modules ## WP3 — The modules
@@ -188,7 +188,7 @@ The catalogue check over the whole catalogue passes.
- **`zsh-autosuggestions` and `zsh-syntax-highlighting`, new.** Each declares its package and - **`zsh-autosuggestions` and `zsh-syntax-highlighting`, new.** Each declares its package and
contributes its loader from the distribution's path, in the `normal` and `last` slots. contributes its loader from the distribution's path, in the `normal` and `last` slots.
**Proof.** The controller's catalogue check over the whole catalogue passes. The modules' tests pass. **Proof.** The controller's catalog check over the whole catalog passes. The modules' tests pass.
A rehearsal composition for a node holding all five shows: A rehearsal composition for a node holding all five shows:
- the `.zshrc` block with the prompt in `normal` and highlighting in `last`; - the `.zshrc` block with the prompt in `normal` and highlighting in `last`;
@@ -227,7 +227,7 @@ The user-scoped units of `mesh-host` #72 stay to-be 38's WP6.
1. Merge WP1 and roll the node-engine. 1. Merge WP1 and roll the node-engine.
2. Merge WP2, and push the controller. 2. Merge WP2, and push the controller.
3. Merge WP3 and WP4, and build the new modules by hand: a new catalogue module's first build is asked 3. Merge WP3 and WP4, and build the new modules by hand: a new catalog module's first build is asked
for, not automatic. for, not automatic.
4. On one server, assign `node-env`, `zsh`, `zsh-autosuggestions` and `zsh-syntax-highlighting`, and 4. On one server, assign `node-env`, `zsh`, `zsh-autosuggestions` and `zsh-syntax-highlighting`, and
push. Then check: push. Then check:
@@ -328,7 +328,7 @@ control-node withholds it until a call to it needs a person's approval. The othe
writes it into, and a word naming that file for its tools bundle. The bundle registers `execute` only writes it into, and a word naming that file for its tools bundle. The bundle registers `execute` only
when the file says `serve`. `zsh_config` also answers whether `execute` is served on the node, and why. when the file says `serve`. `zsh_config` also answers whether `execute` is served on the node, and why.
**Merge order.** The controller first, then the catalogue. Either order composes. With the catalogue **Merge order.** The controller first, then the catalog. Either order composes. With the catalog
alone and the control-node's value set, the self-check would say the holder there is silent. alone and the control-node's value set, the self-check would say the holder there is silent.
**Proof.** The tests ADR 0268 lists under *how it is checked*. Live, after both are merged and rolled **Proof.** The tests ADR 0268 lists under *how it is checked*. Live, after both are merged and rolled
@@ -382,7 +382,7 @@ move, write or delete a file, and a cleanup needed a shell on the machine. Most
project folders, downloads, logs, scratch. project folders, downloads, logs, scratch.
- **A node seat, `node-files`, on every node**, held by a module of its own, `mesh-files`, beside `mesh-reads` - **A node seat, `node-files`, on every node**, held by a module of its own, `mesh-files`, beside `mesh-reads`
in the catalogue; the controller holds the seat's definition beside the other node seats. It has a verb for each file action: list, in the catalog; the controller holds the seat's definition beside the other node seats. It has a verb for each file action: list,
stat, readlink, read, search, write, copy, move, delete, make a folder, change mode, change owner, account and group stat, readlink, read, search, write, copy, move, delete, make a folder, change mode, change owner, account and group
lookup, and the trash's list and restore. Each says what it replaces (ADR 0245): `ls`, `stat`, `readlink`, `cat`, lookup, and the trash's list and restore. Each says what it replaces (ADR 0245): `ls`, `stat`, `readlink`, `cat`,
`grep`, `cp`, `mv`, `rm`, `mkdir`, `chmod`, `chown`, `getent passwd`, `getent group`, `id`, and `ssh <node> cp` for a `grep`, `cp`, `mv`, `rm`, `mkdir`, `chmod`, `chown`, `getent passwd`, `getent group`, `id`, and `ssh <node> cp` for a
@@ -2,7 +2,7 @@
layer: to-be layer: to-be
status: in-progress status: in-progress
code: [mesh-catalog, mesh-controller, mesh-host] code: [mesh-catalog, mesh-controller, mesh-host]
updated: 2026-10-14 updated: 2026-10-22
decisions: decisions:
- "ADR 0369" - "ADR 0369"
- "ADR 0303" - "ADR 0303"
@@ -26,6 +26,9 @@ decisions:
- "ADR 0164" - "ADR 0164"
- "ADR 0165" - "ADR 0165"
- "ADR 0166" - "ADR 0166"
- "ADR 0507"
- "ADR 0508"
- "ADR 0510"
--- ---
# 42. The machines' modules, in order # 42. The machines' modules, in order
@@ -47,7 +50,7 @@ not before. Modules that need none go ahead now.
## How every module moves ## How every module moves
1. Written in the catalogue, with its tools and their tests, and checked by the controller's catalogue 1. Written in the catalog, with its tools and their tests, and checked by the controller's catalog
check. check.
2. Merged, which builds it. 2. Merged, which builds it.
3. Assigned to **the first workstation, the proving machine**, and pushed. Its tools and files are 3. Assigned to **the first workstation, the proving machine**, and pushed. Its tools and files are
@@ -69,14 +72,14 @@ In order:
| 1 | `sudo` | the operator account's escalation, as a drop-in it owns | declares what three modules' tools assume and nothing stated | | 1 | `sudo` | the operator account's escalation, as a drop-in it owns | declares what three modules' tools assume and nothing stated |
| 2 | `localization` | locale, time zone, mesh MCP server keymap | one machine on another zone and keymap | | 2 | `localization` | locale, time zone, mesh MCP server keymap | one machine on another zone and keymap |
| 3 | `time-sync` | timesyncd and its servers | two different daemons across four machines | | 3 | `time-sync` | timesyncd and its servers | two different daemons across four machines |
| 4 | `pacman` | the package manager's configuration, mirrors and their refresh, cache cleaning | mirrors generated once and never again; caches never cleaned | | 4 | `pacman` | the package manager's configuration, mirrors and their refresh, cache cleaning; every node's packages on the bus; upgrades one node at a time, each with a package record, a held kernel, an after-upgrade check and a rollback (ADR 0507, ADR 0508) | mirrors generated once and never again; caches never cleaned; no view of which version runs where; every node upgraded at the same moment with no way back |
| 5 | `logrotate` | the timer and base configuration | rotation running on one machine of four | | 5 | `logrotate` | the timer and base configuration | rotation running on one machine of four |
| 6 | `avahi` | the daemon | on all four, owned by none | | 6 | `avahi` | the daemon | on all four, owned by none |
| 7 | `systemd` | the service manager's tools (to-be 41 WP4) | built, assigned nowhere | | 7 | `systemd` | the service manager's tools (to-be 41 WP4) | built, assigned nowhere |
| 8 | `docker` | the runtime's packages, base configuration, group | four configurations, one owner on one machine | | 8 | `docker` | the runtime's packages, base configuration, group | four configurations, one owner on one machine |
| 9 | `ssh-client` | everything under `~/.ssh` (research 027/03) | a predecessor's entries winning over the mesh's; stale keys | | 9 | `ssh-client` | everything under `~/.ssh` (research 027/03) | a predecessor's entries winning over the mesh's; stale keys |
| 10 | scripts | the operator's own scripts, shared and per role (research 027/03) | under no version control, copied by hand | | 10 | scripts | the operator's own scripts, shared and per role (research 027/03) | under no version control, copied by hand |
| 11 | `kernel` | kernel, microcode, boot entries, and the kernel's run-time settings per machine (transparent huge pages first) | two machines without microcode; huge pages left at the distribution's `always` on a machine whose slow disks they flood (issue 746) | | 11 | `kernel` | kernel, microcode, boot entries, the fallback kernel and its boot entry, and the kernel's run-time settings per machine (transparent huge pages first) | two machines without microcode; huge pages left at the distribution's `always` on a machine whose slow disks they flood (issue 746); only one machine with a fallback kernel |
`systemd`, `pacman` and `docker` hold the three seats that apply resources `systemd`, `pacman` and `docker` hold the three seats that apply resources
(ADR 0207): (ADR 0207):
@@ -101,6 +104,33 @@ mode in force and the file pages each unit holds in huge pages. `docker` holds t
ADR 0164 decides and the capability detected as ADR 0164 decides and the capability detected as
ADR 0165 decides; what of the three is not built yet is tracked by issue 398. ADR 0165 decides; what of the three is not built yet is tracked by issue 398.
**Changed 2026-10-22** (ADR 0507, ADR 0508, ADR 0510; milestone issues 1840 and 1841). `pacman` gains a
process of its own on every node, the keeper, under an account of its own and a bus account of its own. It keeps
the node's installed packages and what an upgrade would change in the module's states, written only on change
(at its start, when the package manager's lock is released, when the mirror list is rewritten, and once a day),
and says each change as an event; the controller keeps the packages each node's modules declare, so the view
answers every node, one that is off included, and names the packages no module declares. Upgrades are runs on
the bus. The weekly timer of ADR 0477 §5, which upgraded every node at the same moment, is dropped. Instead the
nodes upgrade one at a time, in the order of the mesh-level key `upgrade-order`, each after the node before it
passed its after-upgrade check. With no order set no round starts, and every node's health says why. Before each
run a package record is taken and read back. A kernel that an installed DKMS module's own bound excludes is held
with its headers, and said. After each run the after-upgrade check names left configuration files, the
distribution's news, newly failed units, a restart needed, processes still mapping a replaced file and DKMS
modules not built for an installed kernel. A rollback returns a node to a record and says first what it would
change. Only the operator passes a node that stopped a round. The keeper never runs as root: the few acts that
need root go through a root-owned helper process with a closed list of verbs, on a socket only the keeper's
account opens (ADR 0510). The module's health is read from the bus, not from a unit's age.
`kernel` gains the fallback kernel: `linux-lts` and its headers on every machine, with its boot entry on
whichever loader the machine boots with, read from the machine (systemd-boot or GRUB), the default entry left
untouched (ADR 0508 §9-§10, changing ADR 0381 §4). A hook found on a machine that copies every kernel image to
one fixed boot name is taken over: moved into the module's state and named, since it would put the fallback
kernel in the default entry's place. *How it is checked:* the modules' unit tests (the root helper refuses every
verb and argument off its list and every account but the keeper's; the round starts a node only after the one
before it passed; the boot-entry script in stand-in trees of both loaders), an integration test that runs the
keepers of two made-up nodes on a real bus, and, after the delivery, the view answering every node and every
boot menu listing the fallback kernel.
**Added 2026-10-04.** Two more modules for every machine: **Added 2026-10-04.** Two more modules for every machine:
- `power` holds `node-power` (ADR 0211). Other modules contribute code for - `power` holds `node-power` (ADR 0211). Other modules contribute code for
its moments (after boot, before sleep, after waking, before shutdown, on mains, on battery), and it its moments (after boot, before sleep, after waking, before shutdown, on mains, on battery), and it
@@ -169,11 +199,11 @@ both places of both bars once. The bar writes no block that depends on the hardw
the power module's state `power.draw`, one value per machine, which its bundle puts from its own root the power module's state `power.draw`, one value per machine, which its bundle puts from its own root
sampler. The block `shows: state`; the bar's bundle watches it under a read the controller grants the sampler. The block `shows: state`; the bar's bundle watches it under a read the controller grants the
bar for what the modules on its machine offer, and the bar names no module. The battery block is the bar for what the modules on its machine offer, and the bar names no module. The battery block is the
bar's own, reading the kernel. The catalogue's `checks/paths` refuses a module that names another bar's own, reading the kernel. The catalog's `checks/paths` refuses a module that names another
module's path under `/run` or `/var/lib`. module's path under `/run` or `/var/lib`.
The node-engine is delivered first, so the machines report the two capabilities. The controller comes The node-engine is delivered first, so the machines report the two capabilities. The controller comes
next, because it parses `data`, `if-capability` and `renders`. The catalogue comes last. Left open: the next, because it parses `data`, `if-capability` and `renders`. The catalog comes last. Left open: the
draw at the wall (a metered plug or a UPS), a GPU-load block from a generic module, and the headsets. draw at the wall (a metered plug or a UPS), a GPU-load block from a generic module, and the headsets.
## Phase 3 — one machine model ## Phase 3 — one machine model
@@ -183,6 +213,6 @@ and `memory-pressure` (research 027/03).
## How it is checked ## How it is checked
Each module's own tests and the catalogue check, at merge. On the proving machine, each tool answered Each module's own tests and the catalog check, at merge. On the proving machine, each tool answered
through the mesh and each owned file checked in place, before any other machine is assigned. This through the mesh and each owned file checked in place, before any other machine is assigned. This
document's tables are updated as each module lands. document's tables are updated as each module lands.
@@ -146,8 +146,8 @@ a manifest. Not off-site: a lost machine loses its backups with its data, by the
## How it is checked ## How it is checked
The catalogue check (`module check`, and the catalogue-wide test beside it) refuses a provider that The catalog check (`module check`, and the catalog-wide test beside it) refuses a provider that
grants without saying what it keeps for its consumers, an irreplaceable item with neither a backup nor a grants without saying what it keeps for its consumers, an irreplaceable item with neither a backup nor a
redundancy, and any backup line written by hand (ADR 0233). The store rule this design stated before redundancy, and any backup line written by hand (ADR 0233). The store rule this design stated before
was held only by a controller test over the catalogue checked out beside it, never by `module check`. The weekly was held only by a controller test over the catalog checked out beside it, never by `module check`. The weekly
restore test above, and the 48-hour status line, are the running checks. restore test above, and the 48-hour status line, are the running checks.
@@ -68,8 +68,8 @@ Owning repositories: **mesh-controller** (the condition store, watchdogs, `docto
calls, the hand-act log, the facts snapshot), **`mesh-host`** (the node-engine: the apply queue, report calls, the hand-act log, the facts snapshot), **`mesh-host`** (the node-engine: the apply queue, report
order, epoch refusal, the `report` verb, rollback witnessing), **mesh-tools** (the tool runner and the order, epoch refusal, the `report` verb, rollback witnessing), **mesh-tools** (the tool runner and the
mesh MCP server: their heartbeat, passing every argument, health answers), **mesh-catalog** (the mesh MCP server: their heartbeat, passing every argument, health answers), **mesh-catalog** (the
`operator-channel` seat's holder and channels, the watcher, the providers' retirement, the catalogue's `operator-channel` seat's holder and channels, the watcher, the providers' retirement, the catalog's
merge gate), **mesh-sdk** (the TypeScript providers' loop and its retirement), **mesh-lab** (the composition check), **mesh-sdk** (the TypeScript providers' loop and its retirement), **mesh-lab** (the
replays and the induced-failure scenarios). replays and the induced-failure scenarios).
--- ---
@@ -320,7 +320,7 @@ producer that wrote it.
| Probe | Asserts | From | | Probe | Asserts | From |
|---|---|---| |---|---|---|
| D1 | every machine's declaration composes, and passes the node-engine's validation (the validator is a package of `mesh-host` the controller and the merge gate import — one validator) | 236, 263 | | D1 | every machine's declaration composes, and passes the node-engine's validation (the validator is a package of `mesh-host` the controller and the composition check import — one validator) | 236, 263 |
| D2 | every holder of the mesh's resolver answers a machine name for IPv4, and NODATA for IPv6 | 262 | | D2 | every holder of the mesh's resolver answers a machine name for IPv4, and NODATA for IPv6 | 262 |
| D3 | every seat on record has a live holder that answers (`holder-silent`, H3) | 208, 218 | | D3 | every seat on record has a live holder that answers (`holder-silent`, H3) | 208, 218 |
| D4 | every kept archive is held by a manifest | 253 | | D4 | every kept archive is held by a manifest | 253 |
@@ -680,12 +680,12 @@ runs, and a restore that builds a new store beside the live one for a person to
**The facts snapshot.** The controller exports daily, and after any change of machines, assignments or **The facts snapshot.** The controller exports daily, and after any change of machines, assignments or
seats: every machine (a stable pseudonym of the same length as its name, its role, operating system, seats: every machine (a stable pseudonym of the same length as its name, its role, operating system,
C library, architecture, node-engine and tool runner builds), assignments, settings keys and their C library, architecture, node-engine and tool runner builds), assignments, settings keys and their
non-secret values, seats and their holders, the catalogue commit, and the versions the mesh runs of non-secret values, seats and their holders, the catalog commit, and the versions the mesh runs of
the bus server, the store and the node-engine. No secret and no address: an address is replaced by one the bus server, the store and the node-engine. No secret and no address: an address is replaced by one
from a documentation range. It is kept in the artifact store as `facts/latest`, where the build seat from a documentation range. It is kept in the artifact store as `facts/latest`, where the build seat
reads it. reads it.
**The merge gate.** In mesh-controller, `mesh-host` and mesh-catalog, a check composes every machine of **The composition check.** In mesh-controller, `mesh-host` and mesh-catalog, a check composes every machine of
the snapshot with the change applied and runs the node-engine's validator over each. A change that the snapshot with the change applied and runs the node-engine's validator over each. A change that
makes any machine fail to compose or validate fails its check, naming the machine's role and the makes any machine fail to compose or validate fails its check, naming the machine's role and the
module. The resolver module's tests run under both C libraries the snapshot lists. A test in each core module. The resolver module's tests run under both C libraries the snapshot lists. A test in each core
@@ -768,7 +768,7 @@ replay covers what must not be done to it on purpose, and every rule keeps a liv
- **It runs on the build seat**, asked by the controller when the forge's announcer says a pull request's - **It runs on the build seat**, asked by the controller when the forge's announcer says a pull request's
head moved: the repository's own `merge-check.sh` in the mesh's Go toolchain with no container runtime head moved: the repository's own `merge-check.sh` in the mesh's Go toolchain with no container runtime
socket, beside a throwaway store and bus of the versions the snapshot says run, then mesh-lab's replays. socket, beside a throwaway store and bus of the versions the snapshot says run, then mesh-lab's replays.
The verdict is the head commit's status `mesh/merge-gate`, an error never a pass. A catalogue change is The verdict is the head commit's status `mesh/merge-gate`, an error never a pass. A catalog change is
judged by the controller the mesh runs; a node-engine change by it built with the change's validator. judged by the controller the mesh runs; a node-engine change by it built with the change's validator.
- **Replays live where their incident is**: a component's logic in its repository as `TestReplay<issue>`, - **Replays live where their incident is**: a component's logic in its repository as `TestReplay<issue>`,
what the mesh runs (the bus's release, the resolver under musl and glibc) in mesh-lab's `replays/`, all what the mesh runs (the bus's release, the resolver under musl and glibc) in mesh-lab's `replays/`, all
@@ -868,7 +868,7 @@ the `data` verb; `cleanup` extended to a module's own retired data, deleted by t
holder after a last restore point; the controller's grant names `node-backup.backed-up`. Built on holder after a last restore point; the controller's grant names `node-backup.backed-up`. Built on
branches in mesh-controller (migration 0072), `mesh-host` (the installer's user list; the kept-directory branches in mesh-controller (migration 0072), `mesh-host` (the installer's user list; the kept-directory
report), mesh-catalog (the holder's measuring and deletion, every module's data, the providers' held report), mesh-catalog (the holder's measuring and deletion, every module's data, the providers' held
sizes), the media catalogue and the photo application, not yet merged. **Not yet:** the TypeScript sizes), the media catalog and the photo application, not yet merged. **Not yet:** the TypeScript
providers saying their consumers' sizes (until then a consumer at two of them is `data-held-twice`, a providers saying their consumers' sizes (until then a consumer at two of them is `data-held-twice`, a
warning, not compared), and SMART read under an array. warning, not compared), and SMART read under an array.
@@ -907,7 +907,7 @@ a build whose migration cannot be undone; the verdicts of the next three core bu
| Repository | Delivers | | Repository | Delivers |
|---|---| |---|---|
| mesh-controller | the facts snapshot and S14 | | mesh-controller | the facts snapshot and S14 |
| mesh-controller, `mesh-host`, mesh-catalog | the compose-and-validate merge gate; versions tested as run; the resolver's tests under both C libraries | | mesh-controller, `mesh-host`, mesh-catalog | the compose-and-validate check; versions tested as run; the resolver's tests under both C libraries |
| mesh-lab | R4, R5 and the rest of the window's incidents; running the replays on every core merge | | mesh-lab | R4, R5 and the rest of the window's incidents; running the replays on every core merge |
**Done when:** the replays of 236, 262, 263 and 266 fail on the commit before their fix and pass after; **Done when:** the replays of 236, 262, 263 and 266 fail on the commit before their fix and pass after;
@@ -921,7 +921,7 @@ the verdict as their status, and `merge-check.sh`; in `mesh-host` `merge-check.s
installer's user list; in mesh-lab the replays of 262 and 266, the register and the prover; in hq the installer's user list; in mesh-lab the replays of 262 and 266, the register and the prover; in hq the
replay rule in `cycle.py`. The prover ran the replays of **236, 262, 263, 266 and 273: each fails on the replay rule in `cycle.py`. The prover ran the replays of **236, 262, 263, 266 and 273: each fails on the
commit before its fix** (236's check did not exist there) **and passes on it**. **Not yet:** R4, R5 and the commit before its fix** (236's check did not exist there) **and passes on it**. **Not yet:** R4, R5 and the
rest of the window's incidents as replays; the merge gate's first runs against the live mesh's snapshot; rest of the window's incidents as replays; the composition check's first runs against the live mesh's snapshot;
the status required to merge, which is the operator's setting on the forge; mesh-tools' and mesh-sdk's the status required to merge, which is the operator's setting on the forge; mesh-tools' and mesh-sdk's
`merge-check.sh`. `merge-check.sh`.
@@ -932,7 +932,7 @@ and "the commit is the build at hand"), on branches, not yet merged:
| Repository | Delivers | | Repository | Delivers |
|---|---| |---|---|
| mesh-controller | the planner's one mapping of a changed file onto modules and its one answer of a merge's reach, asked by the merge handler, the what-if, the gate and the check; a file no build reads touches nothing (issue 280's *left open*); every pull request answered — the gate when it reaches a module, the repository's own check for the mesh's repositories, a pass that says so otherwise; the gate run by the build seat with its judge chosen from the graph, composing the plan's definitions only, failing only a manifest problem the change brings; the delivery plan computed and carried with the verdict; a build of a commit off its module's trunk recorded and never registered; its own manifest naming every verb of its seat | | mesh-controller | the planner's one mapping of a changed file onto modules and its one answer of a merge's reach, asked by the merge handler, the what-if, the gate and the check; a file no build reads touches nothing (issue 280's *left open*); every pull request answered — the gate when it reaches a module, the repository's own check for the mesh's repositories, a pass that says so otherwise; the gate run by the build seat with its judge chosen from the graph, composing the plan's definitions only, failing only a manifest problem the change brings; the delivery plan computed and carried with the verdict; a build of a commit off its module's trunk recorded and never registered; its own manifest naming every verb of its seat |
| mesh-catalog | the forge's announcer saying, with every pull request, the directories holding a module at its head, the files it deletes and whether it has a `merge-check.sh`; both statuses and the delivery plan on the pull request; `gitea_branch_protection_get` and `gitea_branch_protection_set`; the catalogue's own check | | mesh-catalog | the forge's announcer saying, with every pull request, the directories holding a module at its head, the files it deletes and whether it has a `merge-check.sh`; both statuses and the delivery plan on the pull request; `gitea_branch_protection_get` and `gitea_branch_protection_set`; the catalog's own check |
| `mesh-host`, mesh-tools, mesh-sdk, mesh-lab, mesh-media-catalog, and the applications built from their own repositories | each its own `merge-check.sh`; mesh-tools' tests each on a bus of their own | | `mesh-host`, mesh-tools, mesh-sdk, mesh-lab, mesh-media-catalog, and the applications built from their own repositories | each its own `merge-check.sh`; mesh-tools' tests each on a bus of their own |
| mesh-tools-go, mesh-tools | a C compiler and Python in the Go toolchain, git in the TypeScript one | | mesh-tools-go, mesh-tools | a C compiler and Python in the Go toolchain, git in the TypeScript one |
| hq | its own `merge-check.sh`, running `records.py`, `index.py` and `cycle.py` | | hq | its own `merge-check.sh`, running `records.py`, `index.py` and `cycle.py` |
@@ -696,7 +696,7 @@ Each phase ends at its own *done when*. Owning repositories from [`repos.md`](..
|---|---|---|---| |---|---|---|---|
| **1 — Fix the first holder in place** | D1–D4 in the output seat's holder and the watcher, then D5–D10; no change of shape. The operator then makes the two bots and the mesh is given their values through the controller | mesh-catalog | the holder's tests for D1–D4 pass; a live test message reaches the phone from both the holder and the watcher | | **1 — Fix the first holder in place** | D1–D4 in the output seat's holder and the watcher, then D5–D10; no change of shape. The operator then makes the two bots and the mesh is given their values through the controller | mesh-catalog | the holder's tests for D1–D4 pass; a live test message reaches the phone from both the holder and the watcher |
| **2 — The desk's actions** | `node-notifier.send` gains actions; the dunst holder emits the chosen one; the launcher's prompt returns typed text; the screen-lock holder emits lock and idle changes | mesh-catalog | a notification with two actions returns the chosen token as an event; locking emits an event | | **2 — The desk's actions** | `node-notifier.send` gains actions; the dunst holder emits the chosen one; the launcher's prompt returns typed text; the screen-lock holder emits lock and idle changes | mesh-catalog | a notification with two actions returns the chosen token as an event; locking emits an event |
| **3 — The seats, and the router** | the kinded bench in the seat set; `kind` and `capabilities` on a claim; `channel-capabilities/1` and its contract tests; publishing on a seat's event subjects and its grant; `channel` and `intake` seats; the output seat's holder becomes the router and holds `channel/desktop` and `intake/desktop`; presence as current state; the operator's identity list and identity check in the controller, the router's `trusted` stamping and untrusted input (§4); the content rule allowing machine names, references and `detail`, refusals naming the offending part (§9) | mesh-controller (seat set, claim fields, registration refusals, grants, the identity list and check); mesh-sdk and mesh-tools (seat-event publishing in the shared library and the tool runner); mesh-catalog (the seats' definitions, the router) | the catalogue refuses an unknown capability and a second holder of one kind; a message is routed to the desk by capability and context; an envelope from an identity not on the list is re-emitted untrusted; a path is refused to its sender by name, and travels as a reference the desk opens | | **3 — The seats, and the router** | the kinded bench in the seat set; `kind` and `capabilities` on a claim; `channel-capabilities/1` and its contract tests; publishing on a seat's event subjects and its grant; `channel` and `intake` seats; the output seat's holder becomes the router and holds `channel/desktop` and `intake/desktop`; presence as current state; the operator's identity list and identity check in the controller, the router's `trusted` stamping and untrusted input (§4); the content rule allowing machine names, references and `detail`, refusals naming the offending part (§9) | mesh-controller (seat set, claim fields, registration refusals, grants, the identity list and check); mesh-sdk and mesh-tools (seat-event publishing in the shared library and the tool runner); mesh-catalog (the seats' definitions, the router) | the catalog refuses an unknown capability and a second holder of one kind; a message is routed to the desk by capability and context; an envelope from an identity not on the list is re-emitted untrusted; a path is refused to its sender by name, and travels as a reference the desk opens |
| **4 — Asks, and operator messages** | `ask`, `ask cancel`, `asks`, `answer`; the events; kinds, life, limits, batching, history; escalation; addressing, the register of agents with kept messages, the responder; the untrusted-input rule in every agent module's instructions (§5) | mesh-catalog | the router tests of ADR 0234 pass; live: an agent's question answered at the desk, and with the desk locked, on the phone; `@mesh status` answered; a message to a stopped agent kept and said | | **4 — Asks, and operator messages** | `ask`, `ask cancel`, `asks`, `answer`; the events; kinds, life, limits, batching, history; escalation; addressing, the register of agents with kept messages, the responder; the untrusted-input rule in every agent module's instructions (§5) | mesh-catalog | the router tests of ADR 0234 pass; live: an agent's question answered at the desk, and with the desk locked, on the phone; `@mesh status` answered; a message to a stopped agent kept and said |
| **5a — Asks on the bus** (ADR 0259) | the kinded bench, caller-named verbs, `proof` subjects and the records users read (controller); a bundle's seat traffic, compare-and-set state and its own account (tool runner); the asker's and channel's types (shared library); the router's asks, tokens, identities, judging, warrants, expiry and linking with its hour (messenger); `telegram` and `desk-channel`; the controller asking for its conditions' actions and acting on warrants | mesh-controller, mesh-tools, mesh-sdk, mesh-catalog | a held delivery released from the phone; the hand-act reads "the operator, via telegram, user id verified"; a desk Silence goes through an ask | | **5a — Asks on the bus** (ADR 0259) | the kinded bench, caller-named verbs, `proof` subjects and the records users read (controller); a bundle's seat traffic, compare-and-set state and its own account (tool runner); the asker's and channel's types (shared library); the router's asks, tokens, identities, judging, warrants, expiry and linking with its hour (messenger); `telegram` and `desk-channel`; the controller asking for its conditions' actions and acting on warrants | mesh-controller, mesh-tools, mesh-sdk, mesh-catalog | a held delivery released from the phone; the hand-act reads "the operator, via telegram, user id verified"; a desk Silence goes through an ask |
| **5 — Authorise, with TOTP** | `authorises` in the verb table; the router's checks for the code; direct calls refused, break-glass; `retire approve` takes `expect`; the hand-act fields; the TOTP seed as the router's state, enrolled at a terminal with ten recovery codes; `factor recover`, `factor status`, the local-only break-glass enrolment; no P2 tier before recovery codes exist; optional key enrolment; the self-check probe for the away channel; agents' grants lose the authorising verbs | mesh-controller; mesh-catalog (agents' grants, the desk's code prompt) | one controller test per refusal passes; at the desk, approve needs a code; a recovery drill re-enrols with a recovery code, and is announced | | **5 — Authorise, with TOTP** | `authorises` in the verb table; the router's checks for the code; direct calls refused, break-glass; `retire approve` takes `expect`; the hand-act fields; the TOTP seed as the router's state, enrolled at a terminal with ten recovery codes; `factor recover`, `factor status`, the local-only break-glass enrolment; no P2 tier before recovery codes exist; optional key enrolment; the self-check probe for the away channel; agents' grants lose the authorising verbs | mesh-controller; mesh-catalog (agents' grants, the desk's code prompt) | one controller test per refusal passes; at the desk, approve needs a code; a recovery drill re-enrols with a recovery code, and is announced |
File diff suppressed because it is too large Load Diff
@@ -257,7 +257,7 @@ health needs a record of its own.
- **`module check`** refuses a `health` field naming an endpoint the module does not declare under - **`module check`** refuses a `health` field naming an endpoint the module does not declare under
`listens`, a port or an address, an interval, timeout, count or grace outside its bounds, a tool the module `listens`, a port or an address, an interval, timeout, count or grace outside its bounds, a tool the module
does not serve, or a `tool` kind with no other kind beside it on the module. does not serve, or a `tool` kind with no other kind beside it on the module.
- **The bed.** mesh-lab provides a throwaway container runtime that the catalogue's check, run on the build - **The bed.** mesh-lab provides a throwaway container runtime that the catalog's check, run on the build
seat (ADR 0237), seat (ADR 0237),
uses to start every resource whose declaration or image changed, alone, and read its check. It must say uses to start every resource whose declaration or image changed, alone, and read its check. It must say
healthy within its grace. A check that names a provision in `needs` gets no provider on the bed, so there it healthy within its grace. A check that names a provision in `needs` gets no provider on the bed, so there it
@@ -273,7 +273,7 @@ health needs a record of its own.
nothing long-lived and need no declaration. nothing long-lived and need no declaration.
1. **Liveness first, with no declarations** (Phase A). From then every long-running resource is judged. The 1. **Liveness first, with no declarations** (Phase A). From then every long-running resource is judged. The
catalogue's count of long-running resources without `health` is written down and starts. catalog's count of long-running resources without `health` is written down and starts.
2. **The seven modules whose images ship checks** adopt them by name; for the two that were wrong in the 2. **The seven modules whose images ship checks** adopt them by name; for the two that were wrong in the
mesh's configuration, the corrected check is what the bed proves. 19 containers are covered at once. mesh's configuration, the corrected check is what the bed proves. 19 containers are covered at once.
3. **The other container modules, and the containers without an image check in three of the seven,** declare 3. **The other container modules, and the containers without an image check in three of the seven,** declare
@@ -384,11 +384,11 @@ health of this design, not yet through the machine's own judgement as §10 does
|---|---|---|---| |---|---|---|---|
| A — liveness and the statement | `mesh-host` (node-engine) | liveness for every long-running resource; restarts counted and kept; the state per resource in the report; the change event and its repetition; nothing restarted on health | the node-engine tests of ADR 0240 rules 1 and 6 pass; every machine's report carries a state for every long-running resource | | A — liveness and the statement | `mesh-host` (node-engine) | liveness for every long-running resource; restarts counted and kept; the state per resource in the report; the change event and its repetition; nothing restarted on health | the node-engine tests of ADR 0240 rules 1 and 6 pass; every machine's report carries a state for every long-running resource |
| | mesh-controller | the last state per machine; `module.<module>.<machine>.unhealthy` on the second statement, cleared on the first that does not say it; the gate reading the stated health; `node show` and `status` | the controller tests of rule 4 pass; mesh-lab's replay of the crash loop fails its gate within the bound; a module stopped on purpose on the live mesh is raised and cleared | | | mesh-controller | the last state per machine; `module.<module>.<machine>.unhealthy` on the second statement, cleared on the first that does not say it; the gate reading the stated health; `node show` and `status` | the controller tests of rule 4 pass; mesh-lab's replay of the crash loop fails its gate within the bound; a module stopped on purpose on the live mesh is raised and cleared |
| B — the field | mesh-controller | `health` parsed on every long-running resource; `module check`'s refusals; the catalogue-wide parse | a test per refusal; the whole catalogue passes `module check` | | B — the field | mesh-controller | `health` parsed on every long-running resource; `module check`'s refusals; the catalog-wide parse | a test per refusal; the whole catalog passes `module check` |
| | `mesh-host` (node-engine) | the scheduler and the kinds: http, tcp, unit itself; exec and runtime as the container's check; tool through the tool runner | the rule 3 tests pass; the replay of issue 145 raises the consumer within two looks | | | `mesh-host` (node-engine) | the scheduler and the kinds: http, tcp, unit itself; exec and runtime as the container's check; tool through the tool runner | the rule 3 tests pass; the replay of issue 145 raises the consumer within two looks |
| C — the provider hold | mesh-controller | `needs` read against the provider composed for the consumer; the consumer's finding held under the provider's condition; the consumer's gate waiting | the rule 5 test (one provider, three consumers, one condition) passes | | C — the provider hold | mesh-controller | `needs` read against the provider composed for the consumer; the consumer's finding held under the provider's condition; the consumer's gate waiting | the rule 5 test (one provider, three consumers, one condition) passes |
| D — the proof | mesh-lab, mesh-catalog | the bed; the catalogue's check starting every changed resource on it; adopted image checks proved | the replay of the studio's false *unhealthy* fails the bed, not a machine | | D — the proof | mesh-lab, mesh-catalog | the bed; the catalog's check starting every changed resource on it; adopted image checks proved | the replay of the studio's false *unhealthy* fails the bed, not a machine |
| E — the migration | mesh-catalog, mesh-controller | the declarations of §9 steps 2–5; the count kept in the catalogue and its test; `module check` refusing after the date | the count is zero, or the date has passed and `module check` refuses | | E — the migration | mesh-catalog, mesh-controller | the declarations of §9 steps 2–5; the count kept in the catalog and its test; `module check` refusing after the date | the count is zero, or the date has passed and `module check` refuses |
| F — the machine's network | `mesh-host` (node-engine), `mesh-controller`, `mesh-catalog` | §10: the five parts judged on two looks and stated; the three conditions said once; the gate waiting on another's; `node show` | the tests of ADR 0241 pass; the drill's replay raises and clears; a VPN connect on the laptop is raised naming its writer and cleared by the reconcile | | F — the machine's network | `mesh-host` (node-engine), `mesh-controller`, `mesh-catalog` | §10: the five parts judged on two looks and stated; the three conditions said once; the gate waiting on another's; `node show` | the tests of ADR 0241 pass; the drill's replay raises and clears; a VPN connect on the laptop is raised naming its writer and cleared by the reconcile |
| G — the machine's hardware | `mesh-catalog` | §11: the `sensors` module, its tools, the drive reader and the fan watch, both checks declared | its tests pass; `module check` accepts its health; on the first machine both checks are stated healthy | | G — the machine's hardware | `mesh-catalog` | §11: the `sensors` module, its tools, the drive reader and the fan watch, both checks declared | its tests pass; `module check` accepts its health; on the first machine both checks are stated healthy |
@@ -448,7 +448,7 @@ open:
composes the field — the endpoint as the port the machine published it on, the defaults written out — only composes the field — the endpoint as the port the machine published it on, the defaults written out — only
for a machine whose newest statement says so, and strips it for an older engine, which parses strictly for a machine whose newest statement says so, and strips it for an older engine, which parses strictly
and would refuse the whole declaration. So the engine rolls out first, then the controller, and the and would refuse the whole declaration. So the engine rolls out first, then the controller, and the
catalogue declares only once that controller runs: until then the merge gate refuses a declaration, which catalog declares only once that controller runs: until then the composition check refuses a declaration, which
is the protection working. is the protection working.
- **The engine's looks.** http and tcp from the machine to the published port; a redirect is an answer and - **The engine's looks.** http and tcp from the machine to the published port; a redirect is an answer and
is not followed; a certificate is not judged. A unit's readiness is read from the show liveness already is not followed; a certificate is not judged. A unit's readiness is read from the show liveness already
@@ -481,8 +481,8 @@ machines: one condition, urgent, listing all three; their gates wait; after reco
failing is raised), which fails with four conditions when the hold is taken out. failing is raised), which fails with four conditions when the hold is taken out.
**Phase D — the bed.** In mesh-lab's replays, which every merge check of a module of the graph already runs **Phase D — the bed.** In mesh-lab's replays, which every merge check of a module of the graph already runs
from the lab's trunk with the runtime and the change's catalogue — so nothing else had to change for the from the lab's trunk with the runtime and the change's catalog — so nothing else had to change for the
catalogue's check to start it. It proves the resources whose declared check or image the change touches catalog's check to start it. It proves the resources whose declared check or image the change touches
(every declared one when it cannot tell), each alone: its literal environment, the files its module writes (every declared one when it cannot tell), each alone: its literal environment, the files its module writes
in full, an empty place for every other mount, no secret and no provider. A program that stays up while its in full, an empty place for every other mount, no secret and no provider. A program that stays up while its
check does not see it fails the change; one that does not stay up alone is said and left to the first check does not see it fails the change; one that does not stay up alone is said and left to the first
@@ -490,15 +490,15 @@ machine's gate; a mesh-built image and a tool or unit check are said. The replay
bound to the address its hostname names, and an image check asking the loopback — fails the bed, and passes bound to the address its hostname names, and an image check asking the loopback — fails the bed, and passes
once the hostname is set to every address. once the hostname is set to every address.
**Phase E — the migration.** The catalogue keeps the count in a file of its own, and its merge check fails **Phase E — the migration.** The catalog keeps the count in a file of its own, and its merge check fails
a change that raises it and one that lowers it without writing the new number. It starts at 93 long-running a change that raises it and one that lowers it without writing the new number. It starts at 93 long-running
resources. The first batch takes it to 70: the image checks of the seven modules that ship them, adopted where resources. The first batch takes it to 70: the image checks of the seven modules that ship them, adopted where
they read healthy on the live mesh — except the mail antivirus, whose six-minute start is past the bound, they read healthy on the live mesh — except the mail antivirus, whose six-minute start is past the bound,
and the mail cache, whose image ships none — and tcp or http on endpoints four services already declare. and the mail cache, whose image ships none — and tcp or http on endpoints four services already declare.
Still to do for these phases' "done when": each read on the live mesh once rolled out in order — the Still to do for these phases' "done when": each read on the live mesh once rolled out in order — the
node-engine, then the controller, then the catalogue's declarations; the count's first live reading; and node-engine, then the controller, then the catalog's declarations; the count's first live reading; and
the bed's first run on a catalogue change at the build seat. the bed's first run on a catalog change at the build seat.
## As built — Phase F ## As built — Phase F
+18 -9
View File
@@ -1,22 +1,24 @@
--- ---
layer: to-be layer: to-be
status: in-progress status: in-progress
code: [hq 00-META/checks/words.py, mesh-catalog merge-check.sh] code: [hq 00-META/checks/words_test.go, mesh-catalog merge-check.sh]
updated: 2026-10-18 updated: 2026-10-20
decisions: decisions:
- "ADR 0429" - "ADR 0429"
- "ADR 0244" - "ADR 0244"
- "ADR 0006" - "ADR 0006"
- "ADR 0008" - "ADR 0008"
- "ADR 0302" - "ADR 0302"
- "ADR 0354"
- "ADR 0458"
--- ---
# 49 — The mesh in domains # 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 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.** 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, 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 and the domain decides what that word means. The glossary
@@ -27,7 +29,7 @@ shape — what each is for, where its records live, and how they depend on one a
| Domain | Purpose | Its records live in | | Domain | Purpose | Its records live in |
|---|---|---| |---|---|---|
| **Module** | to say what one module is, in the manifest every other domain reads | the catalogue, one manifest per module | | **Module** | to say what one module is, in the manifest every other domain reads | the catalog, one manifest per module |
| **Core** | to keep the controller, the bus, the store and every node's engine running and agreed | the controller and the store | | **Core** | to keep the controller, the bus, the store and every node's engine running and agreed | the controller and the store |
| **Placement** | to decide, send and apply what each node runs, and say why | the controller's `inventory` database | | **Placement** | to decide, send and apply what each node runs, and say why | the controller's `inventory` database |
| **Provisioning** | to resolve which module serves a provision for which consumer, and wire the two | the controller | | **Provisioning** | to resolve which module serves a provision for which consumer, and wire the two | the controller |
@@ -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 | | **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 | | **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 | | **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, Beside them, **the record** is this repository's own vocabulary — research, decisions, designs,
issues, playbooks, checks — listed because its words meet the mesh's. 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 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. 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 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 to send. An authorising answer acts in whichever domain asked, through that domain's own verb: the
conversation owns the asking, never the act. conversation owns the asking, never the act.
@@ -118,14 +127,14 @@ rule stands with the new word: where a domain's records live in the controller,
## How it is checked ## How it is checked
- **In this repository**, the hq check `words.py` runs on every pull request as part of the repository - **In this repository**, the hq check (the words test, `go test ./...`) runs on every pull request as part of the repository
check. It reads the glossary's *Not:* and *Identifier* lines and fails on a retired word or a bare check. It reads the glossary's *Not:* and *Identifier* lines and fails on a retired word or a bare
identifier in running prose — quotations, code and link targets excepted — in `00-META/`, both layers of identifier in running prose — quotations, code and link targets excepted — in `00-META/`, both layers of
the designs, `AGENTS.md`, `README.md`, and research and issues dated from the decision onward. It fails the designs, `AGENTS.md`, `README.md`, and research and issues dated from the decision onward. It fails
on a head word defined twice or also retired. Decision records are never checked. A document that cannot on a head word defined twice or also retired. Decision records are never checked. A document that cannot
be reworded at once is named with a date in the check's allowance list; a graduated research effort may be reworded at once is named with a date in the check's allowance list; a graduated research effort may
be kept there, as a record of what was said. be kept there, as a record of what was said.
- **In the catalogue**, its own repository check reads a copy of the words retired for the tools and fails - **In the catalog**, its own repository check reads a copy of the words retired for the tools and fails
when what a module's tools show an agent uses one. The copy and the glossary are compared whenever both when what a module's tools show an agent uses one. The copy and the glossary are compared whenever both
are at hand; a change retiring a tools word changes both repositories. are at hand; a change retiring a tools word changes both repositories.
- **Homonyms** are checked by review: the reviewer looks for the bare word in the diff. - **Homonyms** are checked by review: the reviewer looks for the bare word in the diff.
@@ -136,8 +145,8 @@ rule stands with the new word: where a domain's records live in the controller,
`node-build-agent`, the licence manager's `release`, the controller's `plan`, `doctor` and queue verbs, `node-build-agent`, the licence manager's `release`, the controller's `plan`, `doctor` and queue verbs,
the packet filter module's tool — each in its own repository's change. the packet filter module's tool — each in its own repository's change.
- **The descriptions served by the core's own repositories** (the controller's, the node-engine's and the - **The descriptions served by the core's own repositories** (the controller's, the node-engine's and the
tool runner's verbs) are outside the catalogue and so outside its check. tool runner's verbs) are outside the catalog and so outside its check.
- **A probe of the served descriptions** in the self-check, once the catalogue's check has run a while. - **A probe of the served descriptions** in the self-check, once the catalog's check has run a while.
- **A domain on each decision record**, which would let the reading order be printed per domain; a change - **A domain on each decision record**, which would let the reading order be printed per domain; a change
to the record schema, not made here. to the record schema, not made here.
- **The reverse rule** — a design defining a word must find it in the glossary — proposed by research 034 - **The reverse rule** — a design defining a word must find it in the glossary — proposed by research 034
@@ -48,7 +48,7 @@ another. A *routing domain* is a domain whose names a resolver sends to one link
|---|---|---| |---|---|---|
| the seat `node-resolver` | the controller's set | a node seat, its verbs `routes`, `route`, `unroute`, required of every holder | | the seat `node-resolver` | the controller's set | a node seat, its verbs `routes`, `route`, `unroute`, required of every holder |
| the stepping back | the controller's composition | where a module holds `node-resolver` and renders `/etc/resolv.conf`, the uplink holder's rendering of it is left out | | the stepping back | the controller's composition | where a module holds `node-resolver` and renders `/etc/resolv.conf`, the uplink holder's rendering of it is left out |
| `systemd-resolved` | the catalogue | holds the seat, provides `split-dns` at the machine's reach, writes the file and resolved's drop-in, runs the guard | | `systemd-resolved` | the catalog | holds the seat, provides `split-dns` at the machine's reach, writes the file and resolved's drop-in, runs the guard |
| the guard | `systemd-resolved`'s process, as root | keeps the file the module's, keeps an outside write, serves the verbs on the machine's socket | | the guard | `systemd-resolved`'s process, as root | keeps the file the module's, keeps an outside write, serves the verbs on the machine's socket |
| the adapter | `forticlient`'s process, as root | reads the client's write, routes its domains over its tunnel, takes the write, takes the route away when the tunnel goes | | the adapter | `forticlient`'s process, as root | reads the client's write, routes its domains over its tunnel, takes the write, takes the route away when the tunnel goes |
+2 -2
View File
@@ -56,8 +56,8 @@ The glossary holds the three words.
| part | where | does | | part | where | does |
|---|---|---| |---|---|---|
| seats `node-nfs-server` and `node-mounts` | the controller's closed set | node seats, capacity 1, with their verbs | | seats `node-nfs-server` and `node-mounts` | the controller's closed set | node seats, capacity 1, with their verbs |
| `nfs-server` | the catalogue | holds `node-nfs-server`, provides the provision, writes its export file, runs the service, opens 2049 to the private network | | `nfs-server` | the catalog | holds `node-nfs-server`, provides the provision, writes its export file, runs the service, opens 2049 to the private network |
| `mounts` | the catalogue | holds `node-mounts`, writes a unit pair per share, keeps each share's state, serves the tools | | `mounts` | the catalog | holds `node-mounts`, writes a unit pair per share, keeps each share's state, serves the tools |
| the binding | the controller | gives a consumer the server's name; the consumer asks the server's seat for each share's path and access over the bus | | the binding | the controller | gives a consumer the server's name; the consumer asks the server's seat for each share's path and access over the bus |
| the credential | the vault | per SMB source, the `mounts` module's own secret `smb-password-<source>` (a member of the family `smb-password-*`), issued outside the mesh and given with `secret accept` or typed at the desk prompt, unsealed on the node as a file; the username is the user part of the source's entry in the setting `sources` | | the credential | the vault | per SMB source, the `mounts` module's own secret `smb-password-<source>` (a member of the family `smb-password-*`), issued outside the mesh and given with `secret accept` or typed at the desk prompt, unsealed on the node as a file; the username is the user part of the source's entry in the setting `sources` |
+37 -6
View File
@@ -2,7 +2,7 @@
layer: to-be layer: to-be
status: in-progress status: in-progress
code: [mesh-catalog modules/mesh-issues, mesh-catalog modules/claude-code, mesh-catalog modules/messenger, mesh-sdk] code: [mesh-catalog modules/mesh-issues, mesh-catalog modules/claude-code, mesh-catalog modules/messenger, mesh-sdk]
updated: 2026-10-11 updated: 2026-10-26
decisions: decisions:
- "ADR 0275" - "ADR 0275"
- "ADR 0278" - "ADR 0278"
@@ -13,6 +13,7 @@ decisions:
- "ADR 0291" - "ADR 0291"
- "ADR 0294" - "ADR 0294"
- "ADR 0300" - "ADR 0300"
- "ADR 0493"
--- ---
# 53 — The issue tracker # 53 — The issue tracker
@@ -81,7 +82,10 @@ agent finds them with `mesh_search` ("open an issue") and calls them as `issue-t
| `set` | a number and any of `severity`, `needs`, `suggested-owner` | changes them as a person decides (§ 10) | | `set` | a number and any of `severity`, `needs`, `suggested-owner` | changes them as a person decides (§ 10) |
| `tracking` | optionally condition keys | for every condition key an issue links, the issues that link it and where each stands: status, needs, worker, fix (§ 12) | | `tracking` | optionally condition keys | for every condition key an issue links, the issues that link it and where each stands: status, needs, worker, fix (§ 12) |
| `groom` | optionally `confirm` and a pass's id | the duplicate check over the backlog (§ 9): without `confirm`, starts a dry run in the background and answers its id; with the id alone, its progress and findings; with `confirm` and the id, applies exactly that pass's findings, each checked again | | `groom` | optionally `confirm` and a pass's id | the duplicate check over the backlog (§ 9): without `confirm`, starts a dry run in the background and answers its id; with the id alone, its progress and findings; with `confirm` and the id, applies exactly that pass's findings, each checked again |
| `import` (slice 3) | optionally `dry-run`, a commit | reads every issue folder of hq through the records module and writes each as an issue at its own number; creates what is absent, fills a status the tracker holds empty, never overwrites, never advances the counter; run again, changes nothing and says so | | `import` (slice 3) | optionally `dry-run`, a commit, and `milestones` with a design's number | reads every issue folder of hq through the records module and writes each as an issue at its own number; creates what is absent, fills a status the tracker holds empty, never overwrites, never advances the counter; run again, changes nothing and says so. With `milestones`, it seeds that design's milestone records from its table instead (§ 13) |
| `milestones` | optionally a design, a status, or one milestone as design/id | the milestone records, each with its issues and their count by status; one milestone with its issues' rows and its history (§ 13) |
| `milestone-set` | a design, an id, an outcome, optionally an order, or `announce` | makes a milestone record, or changes its outcome or order; with `announce`, says the record again as its event (§ 13) |
| `milestone-move` | a milestone as design/id, a status and a reason | opens or closes a milestone; closing is refused while an issue linked to it is neither resolved nor wontfix (§ 13) |
Each verb's description says in plain words what it does and what it refuses, without the mesh's internal names, so Each verb's description says in plain words what it does and what it refuses, without the mesh's internal names, so
an agent picks the right one from `mesh_search` alone. A refusal says what was wrong and what would be accepted. an agent picks the right one from `mesh_search` alone. A refusal says what was wrong and what would be accepted.
@@ -100,8 +104,8 @@ the issue's worker, who works on it, as the worker states it
- `located-in`: entries of the registered list of owners (repositories and modules), never free text; - `located-in`: entries of the registered list of owners (repositories and modules), never free text;
- `fixed-by` (pull requests or commits), `amended-design`, `replay` or `replay-none`, and a reason for `wontfix`; - `fixed-by` (pull requests or commits), `amended-design`, `replay` or `replay-none`, and a reason for `wontfix`;
- `notes`: appended, each with who wrote it, when, and the text; - `notes`: appended, each with who wrote it, when, and the text;
- `links`, typed: to another issue (`duplicates`, `relates-to`), a pull request, a decision record, a design, a - `links`, typed: to another issue (`duplicates`, `relates-to`), a pull request, a commit, a decision record, a design,
condition key (the issue **tracks** that condition, § 12) or a delivery; a condition key (the issue **tracks** that condition, § 12), a delivery, or the one milestone it belongs to (§ 13);
- `documents`: the report, and a diagnosis and a resolution when written, each with its author and date, revised in - `documents`: the report, and a diagnosis and a resolution when written, each with its author and date, revised in
place with its history kept (ADR 0291); place with its history kept (ADR 0291);
- `record`: the path of the issue's folder in hq, while one exists; none is opened any more, and the folders are - `record`: the path of the issue's folder in hq, while one exists; none is opened any more, and the folders are
@@ -111,7 +115,7 @@ the issue's worker, who works on it, as the worker states it
kind `depends-on` (§ 10). kind `depends-on` (§ 10).
**The registered list of owners** is a setting of the module, changed through the controller's settings verb like any **The registered list of owners** is a setting of the module, changed through the controller's settings verb like any
other module setting. Slice 1 starts it with the repositories in hq's `00-META/repos.md` and the modules the catalogue other module setting. Slice 1 starts it with the repositories in hq's `00-META/repos.md` and the modules the catalog
registers. registers.
**An issue that has a folder in hq** kept its status in that folder's frontmatter until the import of slice 3 ran; **An issue that has a folder in hq** kept its status in that folder's frontmatter until the import of slice 3 ran;
@@ -167,7 +171,7 @@ event carries a secret, and an issue holds none.
| any other status | no `by` and no `take-over`; the move ends the work | | any other status | no `by` and no `take-over`; the move ends the work |
| `located` | `located-in` names at least one registered owner | | `located` | `located-in` names at least one registered owner |
| `resolved` | the issue is at `fixing` (or already `resolved`, to correct what it holds); `located-in`, and `fixed-by` saying what fixed it | | `resolved` | the issue is at `fixing` (or already `resolved`, to correct what it holds); `located-in`, and `fixed-by` saying what fixed it |
| `resolved`, for a core issue (its `located-in` names the controller, the node-engine, the tool runner, the SDK, or the catalogue's bus, forge or build agent) | `replay` (an id from mesh-lab's replays register) or `replay-none` saying why none is possible (ADR 0237) | | `resolved`, for a core issue (its `located-in` names the controller, the node-engine, the tool runner, the SDK, or the catalog's bus, forge or build agent) | `replay` (an id from mesh-lab's replays register) or `replay-none` saying why none is possible (ADR 0237) |
| `wontfix` | a reason | | `wontfix` | a reason |
A refused move changes nothing and says which rule refused it. The rows for `diagnosing`, `fixing` and `resolved` are A refused move changes nothing and says which rule refused it. The rows for `diagnosing`, `fixing` and `resolved` are
@@ -203,6 +207,7 @@ worker; one that stops moves the issue on, or is stale a day later.
| The triage fills only what is empty, links no cycle, and an issue is ready only when its dependencies' fixes are delivered | the rows of ADR 0279's "How it is checked" | | The triage fills only what is empty, links no cycle, and an issue is ready only when its dependencies' fixes are delivered | the rows of ADR 0279's "How it is checked" |
| The duplicate check acts only on a clear duplicate, never on an older or a resolved issue, once per issue, loudly when it cannot judge | the rows of ADR 0278's "How it is checked" | | The duplicate check acts only on a clear duplicate, never on an older or a resolved issue, once per issue, loudly when it cannot judge | the rows of ADR 0278's "How it is checked" |
| An issue's documents are kept whole, with author, date and history, and the import of hq's folders loses nothing (ADR 0291) | the module's tests of `documents` and of the import, one case per document kind; the comparison of every issue with its folder before the folders are removed, written on issue 427. Tracked by issue 427 | | An issue's documents are kept whole, with author, date and history, and the import of hq's folders loses nothing (ADR 0291) | the module's tests of `documents` and of the import, one case per document kind; the comparison of every issue with its folder before the folders are removed, written on issue 427. Tracked by issue 427 |
| An issue belongs to at most one milestone, linked only by the verb, and only to an open one (ADR 0493 §2 to §4) | the module's table test of the `milestone` and `commit` link kinds, one case per refusal; the test that the triage and the duplicate check never add a `milestone`, `pull-request` or `commit` link; one integration test that starts the module whole on a real bus and makes, links and refuses |
| Live | on the mesh, an agent calls `issue-tracker.open` and gets the next number; two sessions on two nodes open at the same moment and get two different numbers; `list` shows both | | Live | on the mesh, an agent calls `issue-tracker.open` and gets the next number; two sessions on two nodes open at the same moment and get two different numbers; `list` shows both |
## 8. The slices after the first ## 8. The slices after the first
@@ -312,6 +317,32 @@ with no pull request open is **abandoned**.
**tracking line** in every condition's message; the words and the verdict it changes are its own (to-be 45 § 5). **tracking line** in every condition's message; the words and the verdict it changes are its own (to-be 45 § 5).
The controller learns nothing of issues. The controller learns nothing of issues.
## 13. Milestones
**A milestone is a record of the tracker, made from its design's table** (ADR 0493 §1). The state `milestones`, which the
manifest declares beside `issues`, keeps one value per milestone, keyed by its design's number and its id: the id as the
design's table writes it, the outcome, its order in the table, its status (`open` or `closed`, with a reason), who made
and who last moved it and when, and the hq commit and path it was seeded from. It is written by compare-and-set at the
revision read, and its history is the key's kept values with the events `milestone-set` and `milestone-moved`.
**An issue's milestone is the issue's own link, not a list on the milestone.** The link kind `milestone` (its target
design/id, such as 47/R6) is written in the issue's log like any link, so the rule that an issue belongs to at most one
milestone (ADR 0493 §2) is checked inside that issue's one write, refused naming the milestone it is in; a link to a
milestone that does not exist or is closed is refused (§3: a fix found on the way joins the open milestone). A milestone's
issues are read from the issues in memory. Pull requests and commits are linked to the issue they fix the same way, with
the kinds `pull-request` and `commit` (§4); nothing reads them from a description, and the triage never adds one.
**The seed** is `import` with `milestones` and a design's number: it reads the table through the records module, makes
each record that is absent in the table's order, links each issue it names that has no milestone, refuses (and names)
an issue another milestone holds or two rows name, closes a milestone whose issues are all done (wontfix, or resolved
with every live fix delivered, or resolved with no live fix) when it refused none of its row's issues, and run again
changes nothing. Once a design is seeded, the tracker is the only list of its milestones'
issues: the design's table keeps its rows without an Issues column, and the knowledge base's `progress` reads
`milestones` (issue 1781).
Readiness, the one ask per milestone and its release are the delivery seat's (ADR 0493 §7 to §11, issue 1783); the
tracker gives them the records and links they read.
## What it leaves out ## What it leaves out
Priorities beyond severity, notifications beyond the `ready` and `ready-to-verify` events and the stuck-fix warning, full-text or semantic search, the forge's own issue lists, and an index of any kind: Priorities beyond severity, notifications beyond the `ready` and `ready-to-verify` events and the stuck-fix warning, full-text or semantic search, the forge's own issue lists, and an index of any kind:
+2 -2
View File
@@ -228,7 +228,7 @@ the page as the authority on words (ADR 0423, changing ADR 0244).
|---|---| |---|---|
| the knowledge base's own rule on a retired word (§ 4) | the *Not:* and identifier lines of the entries, no longer the page through the records module; it judges a design when it is proposed, as it judges a decision | | the knowledge base's own rule on a retired word (§ 4) | the *Not:* and identifier lines of the entries, no longer the page through the records module; it judges a design when it is proposed, as it judges a decision |
| hq's word check | the entries' *Not:* and identifier lines, and its uniqueness check (a head word heading two entries, or also retired) over the entries; it covers what stays in this repository, and designs leave its coverage | | hq's word check | the entries' *Not:* and identifier lines, and its uniqueness check (a head word heading two entries, or also retired) over the entries; it covers what stays in this repository, and designs leave its coverage |
| the catalogue's repository check | the retired words of no scope or *(tools)* from the entries; its copied list is retired, so hq has no copy to compare | | the catalog's repository check | the retired words of no scope or *(tools)* from the entries; its copied list is retired, so hq has no copy to compare |
| hq's cycle check | the design records, for every design naming its decisions, not files under `03-DESIGN/` | | hq's cycle check | the design records, for every design naming its decisions, not files under `03-DESIGN/` |
Each of the four fails, saying so, when the knowledge base does not answer, and never passes on nothing. Each of the four fails, saying so, when the knowledge base does not answer, and never passes on nothing.
@@ -318,7 +318,7 @@ is done:
8. Every entry of the glossary's Config section is filed under the Config domain, and the domains the entries name 8. Every entry of the glossary's Config section is filed under the Config domain, and the domains the entries name
are the twelve of ADR 0354. are the twelve of ADR 0354.
9. Tests 1 to 8 pass together after the one delivery, run against main and the live knowledge base, with nothing 9. Tests 1 to 8 pass together after the one delivery, run against main and the live knowledge base, with nothing
in this repository, the catalogue or the instruction text pointing at a removed path. in this repository, the catalog or the instruction text pointing at a removed path.
## What it leaves out ## What it leaves out
@@ -88,7 +88,7 @@ and may not be copied to anyone else. The vendor serves only its newest version,
file (a `file://` URL), with `updates_disabled`. Betterbird installs it into every profile, and the person file (a `file://` URL), with `updates_disabled`. Betterbird installs it into every profile, and the person
cannot remove it. cannot remove it.
Owl declares the range of Betterbird versions it supports. The catalogue's check refuses a manifest whose Owl declares the range of Betterbird versions it supports. The catalog's check refuses a manifest whose
Betterbird pin lies outside the range of its Owl pin, so a Betterbird upgrade that Owl does not support yet Betterbird pin lies outside the range of its Owl pin, so a Betterbird upgrade that Owl does not support yet
waits for Owl. waits for Owl.
@@ -205,7 +205,7 @@ module needs.
**The preferences are one entry each.** The setting `prefs` maps a preference's name to its value, so a node's **The preferences are one entry each.** The setting `prefs` maps a preference's name to its value, so a node's
layer overrides one preference, or takes one out with null, and the operator's setup lives in the mesh-wide layer overrides one preference, or takes one out with null, and the operator's setup lives in the mesh-wide
layer, not in the catalogue. The module ships only its policy's locks and the names it keeps at Betterbird's own layer, not in the catalog. The module ships only its policy's locks and the names it keeps at Betterbird's own
default (issue 586). `prefs` refuses a credential's preference, an account's (§4), and the four prefixes the default (issue 586). `prefs` refuses a credential's preference, an account's (§4), and the four prefixes the
settings below own: `calendar.registry.`, `calendar.list.`, `ldap_2.servers.` and `mailnews.tags.`, so each value settings below own: `calendar.registry.`, `calendar.list.`, `ldap_2.servers.` and `mailnews.tags.`, so each value
has one owner. has one owner.
@@ -254,11 +254,11 @@ own layers. With `propose` it proposes the next layer for the operator's Approve
each node's that needs one, each only after the one before it was set or declined, against the layer as it then each node's that needs one, each only after the one before it was set or declined, against the layer as it then
stands and carrying that layer whole (ADR 0277 keeps one open ask per module and machine). It proposes nothing stands and carrying that layer whole (ADR 0277 keeps one open ask per module and machine). It proposes nothing
while a desktop node does not answer, because a layer derived without it would put on every node what only some while a desktop node does not answer, because a layer derived without it would put on every node what only some
hold. The first mesh-wide proposal also carries the operator's setup of `prefs`, which the catalogue held before. hold. The first mesh-wide proposal also carries the operator's setup of `prefs`, which the catalog held before.
## How it is checked ## How it is checked
- **The catalogue's tests:** both pins carry a URL, a version and a sha256; a Betterbird pin outside Owl's range - **The catalog's tests:** both pins carry a URL, a version and a sha256; a Betterbird pin outside Owl's range
is refused; the generated policy file names Owl as `force_installed` from the placed file with is refused; the generated policy file names Owl as `force_installed` from the placed file with
`updates_disabled`, and sets `DisableAppUpdate`; `prefs` refuses the account prefixes and the four of §8; `updates_disabled`, and sets `DisableAppUpdate`; `prefs` refuses the account prefixes and the four of §8;
`accounts` refuses a password-like field; a declared account's user.js holds only the module's own keys; every `accounts` refuses a password-like field; a declared account's user.js holds only the module's own keys; every
@@ -38,7 +38,7 @@ request, and the five, sharing their branch's name, are one delivery group (ADR
future state. The five are merged within one merge window, so they are one batch and one rollout (ADR 0276 future state. The five are merged within one merge window, so they are one batch and one rollout (ADR 0276
§1, §2), in an order declared with `after:` lines in each pull request (ADR 0239 §11): mesh-sdk, then §1, §2), in an order declared with `after:` lines in each pull request (ADR 0239 §11): mesh-sdk, then
`mesh-host`, then mesh-controller, then mesh-catalog, then hq. The release holds every package but T6, R1 and R2: `mesh-host`, then mesh-controller, then mesh-catalog, then hq. The release holds every package but T6, R1 and R2:
the media catalogue and the applications live in repositories of their own and move after it, and only the media catalog and the applications live in repositories of their own and move after it, and only
then can the old forms be refused (R1) and the record say what runs (R2). then can the old forms be refused (R1) and the record say what runs (R2).
**The moves of data run after the delivery, in this order, each a verb that says what it did:** **The moves of data run after the delivery, in this order, each a verb that says what it did:**
@@ -58,7 +58,7 @@ the backlog rollout once it is heard from (ADR 0360 §10).
**Nothing breaks on the way.** ADR 0349 clause 17: each file is filled the old way or the new way, never **Nothing breaks on the way.** ADR 0349 clause 17: each file is filled the old way or the new way, never
both, by the form of its manifest, and the old forms stay accepted until the last module has moved. both, by the form of its manifest, and the old forms stay accepted until the last module has moved.
Inside the release this still holds: the media catalogue and the applications kept in their own Inside the release this still holds: the media catalog and the applications kept in their own
repositories move after it, so the old forms stay accepted until they have (R1). repositories move after it, so the old forms stay accepted until they have (R1).
**Where it stands, 2026-10-17** (the Milestones section below). The packages' branches were merged into **Where it stands, 2026-10-17** (the Milestones section below). The packages' branches were merged into
@@ -151,15 +151,15 @@ glossary says it is the older word. **Proof:** `words.py` passes.
stage.* A file resource takes `"template": "files/<name>"` in place of `content`; the module's build packs stage.* A file resource takes `"template": "files/<name>"` in place of `content`; the module's build packs
`files/` into an archive artifact of its own (the existing `archive` packing); registration and `Resolve` `files/` into an archive artifact of its own (the existing `archive` packing); registration and `Resolve`
read each template's text back from the packed tree into `content`, so composition and the node-engine read each template's text back from the packed tree into `content`, so composition and the node-engine
are unchanged; `module check` and the merge gate read templates from the repository tree beside the are unchanged; `module check` and the composition check read templates from the repository tree beside the
manifest; a file naming both `template` and `content` is refused. **Proof:** composition tests: a module manifest; a file naming both `template` and `content` is refused. **Proof:** composition tests: a module
with its text in `files/` composes byte-identically to the same module with it inline; the merge gate with its text in `files/` composes byte-identically to the same module with it inline; the composition check
judges a change to a template alone. judges a change to a template alone.
**T2 (issue 655) — The twelve modules with copies.** *mesh-catalog. A day.* adwaita, clipmenu, dunst, feh, **T2 (issue 655) — The twelve modules with copies.** *mesh-catalog. A day.* adwaita, clipmenu, dunst, feh,
gnome-keyring, i3, i3status-rust, lemurs, picom, rofi, screen-lock and xdg move to their templates; the gnome-keyring, i3, i3status-rust, lemurs, picom, rofi, screen-lock and xdg move to their templates; the
34 inline copies and the `sameAsSource` tests go. **Proof:** each module's composed declaration on its 34 inline copies and the `sameAsSource` tests go. **Proof:** each module's composed declaration on its
nodes is unchanged (the merge gate's plan says no file changes). nodes is unchanged (the composition check's plan says no file changes).
**T3 (issue 656) — The desktop modules.** *mesh-catalog. A day.* Every remaining module assigned only to **T3 (issue 656) — The desktop modules.** *mesh-catalog. A day.* Every remaining module assigned only to
workstations. **Proof:** as T2. workstations. **Proof:** as T2.
@@ -169,9 +169,9 @@ as T2.
**T5 (issue 658) — The ten modules without a build.** *mesh-catalog. A day.* ca-trust, de-spiegel, invoicing, **T5 (issue 658) — The ten modules without a build.** *mesh-catalog. A day.* ca-trust, de-spiegel, invoicing,
local-model-consumer, matrix, node-env, only-office, sshd, step-ca and supabase gain an archive build for local-model-consumer, matrix, node-env, only-office, sshd, step-ca and supabase gain an archive build for
their templates. **Proof:** as T2; afterwards no manifest in the catalogue carries file text. their templates. **Proof:** as T2; afterwards no manifest in the catalog carries file text.
**T6 (issue 659) — The media catalogue and the applications.** *mesh-media-catalog and each application's **T6 (issue 659) — The media catalog and the applications.** *mesh-media-catalog and each application's
repository. Two days.* **Proof:** as T2, per repository. repository. Two days.* **Proof:** as T2, per repository.
## 2 — Config (ADR 0348) ## 2 — Config (ADR 0348)
@@ -185,10 +185,10 @@ object or list not trusted; a predefined key not on the list or of another type
value falling back to a trusted key; a value neither in a template nor read at runtime; a placeholder value falling back to a trusted key; a value neither in a template nor read at runtime; a placeholder
naming an undeclared value; a `reload-on` or `restart-on` naming an undeclared file; a moved module's naming an undeclared value; a `reload-on` or `restart-on` naming an undeclared file; a moved module's
mergeable file not naming its keys; a mesh word or `puid`/`pgid` in `takes-config`. **Proof:** the registration refusal tests, one per mergeable file not naming its keys; a mesh word or `puid`/`pgid` in `takes-config`. **Proof:** the registration refusal tests, one per
rule; every manifest in the catalogue still registers. rule; every manifest in the catalog still registers.
**K2 (issue 661) — The list of predefined keys.** *mesh-controller and mesh-catalog. A day.* `predefined.json` at the **K2 (issue 661) — The list of predefined keys.** *mesh-controller and mesh-catalog. A day.* `predefined.json` at the
catalogue's root, with `font-size` and `agent-managed-settings` first; the build validates it and every catalog's root, with `font-size` and `agent-managed-settings` first; the build validates it and every
manifest naming a key; the merge check refuses removing a key still named or holding a value; the newest manifest naming a key; the merge check refuses removing a key still named or holding a value; the newest
trunk build's list is published. **Proof:** the merge check refuses a planted removal; an older build's trunk build's list is published. **Proof:** the merge check refuses a planted removal; an older build's
list does not replace a newer one. list does not replace a newer one.
@@ -214,9 +214,9 @@ the controller reads `mesh-config_resolved` where it read its layers (ADR 0349 c
an exception to ADR 0348 §9 that R1 removes), and `settings` answers by forwarding to `config`. **Proof:** composition of an exception to ADR 0348 §9 that R1 removes), and `settings` answers by forwarding to `config`. **Proof:** composition of
every node before and after is byte-identical; `settings` and `config` list the same values. every node before and after is byte-identical; `settings` and `config` list the same values.
**K6 (issue 665) — The catalogue says config.** *mesh-catalog. A day.* The 15 modules using `${setting:}`, the 9 **K6 (issue 665) — The catalog says config.** *mesh-catalog. A day.* The 15 modules using `${setting:}`, the 9
declaring settings and the 4 with `takes-settings` move to the new names and the full declaration. declaring settings and the 4 with `takes-settings` move to the new names and the full declaration.
**Proof:** the catalogue's check finds no old name in a moved module. **Proof:** the catalog's check finds no old name in a moved module.
**K7 (issue 666) — The operator's acceptance on dunst.** *live. An hour.* With every level cleared the first **K7 (issue 666) — The operator's acceptance on dunst.** *live. An hour.* With every level cleared the first
workstation's dunstrc says 10; a mesh font size, then that node's font size, then dunst's own value on workstation's dunstrc says 10; a mesh font size, then that node's font size, then dunst's own value on
@@ -326,7 +326,7 @@ ADR 0349; claude-code reads its managed settings through it.
## 8 — The old forms go (ADR 0349 clause 17) ## 8 — The old forms go (ADR 0349 clause 17)
**R1 (issue 682) — Refused and removed.** *mesh-controller, `mesh-host`, mesh-catalog. Three days.* Once every module of **R1 (issue 682) — Refused and removed.** *mesh-controller, `mesh-host`, mesh-catalog. Three days.* Once every module of
every catalogue has moved: the catalogue's check refuses inline text, `settings`, `${setting:}` and the every catalog has moved: the catalog's check refuses inline text, `settings`, `${setting:}` and the
old contribution forms; the controller's filling passes but placement, its settings, secret and binding old contribution forms; the controller's filling passes but placement, its settings, secret and binding
code and tables (a new migration) go; the node-engine keeps opening sealed values for the core's files code and tables (a new migration) go; the node-engine keeps opening sealed values for the core's files
only; the controller stops reading `mesh-config_resolved`. **Proof:** the controller's test that its only; the controller stops reading `mesh-config_resolved`. **Proof:** the controller's test that its
@@ -345,7 +345,7 @@ each makes true. Whether a milestone's issues are delivered is read from the iss
| # | Outcome | Packages | Issues | | # | Outcome | Packages | Issues |
|---|---|---|---| |---|---|---|---|
| 1 | Config, provisioning, secrets, bindings, collections and runtime asks run the new way; the old forms are still accepted | every package but T6, B3, R1, R2 | 647, 650 to 679 but 659, 681, 684, 685 | | 1 | Config, provisioning, secrets, bindings, collections and runtime asks run the new way; the old forms are still accepted | every package but T6, B3, R1, R2 | 647, 650 to 679 but 659, 681, 684, 685 |
| 2 | The media catalogue and the applications carry their file text as templates | T6 | 659 | | 2 | The media catalog and the applications carry their file text as templates | T6 | 659 |
| 3 | The other providers answer their consumers; the old forms are refused; the record says what runs | B3, R1, R2 | 680, 682, 683 | | 3 | The other providers answer their consumers; the old forms are refused; the record says what runs | B3, R1, R2 | 680, 682, 683 |
## What is deliberately not here ## What is deliberately not here
@@ -128,7 +128,7 @@ identity present on a machine into that machine's declaration, and the node-engi
resource. A login that exists is adopted under its name, uid and home; the mesh never renames or deletes one. An resource. A login that exists is adopted under its name, uid and home; the mesh never renames or deletes one. An
agent's machine account has its own home and group, no password, no root group and no sudo rule, and the node-engine agent's machine account has its own home and group, no password, no root group and no sudo rule, and the node-engine
judges on every look that it cannot become root without a person, by the ways ADR 0266 §12 to §17 list. No module declares a login for a judges on every look that it cannot become root without a person, by the ways ADR 0266 §12 to §17 list. No module declares a login for a
person or an agent: claude-code's `agent-account` resource goes, and the catalogue's check refuses a manifest that person or an agent: claude-code's `agent-account` resource goes, and the catalog's check refuses a manifest that
declares one. The node record's operator account and agent account are read from the identities, no longer stated declares one. The node record's operator account and agent account are read from the identities, no longer stated
at the controller's terminal or in the mesh-config snapshot; the machine facts `account`, `agent-account` and their at the controller's terminal or in the mesh-config snapshot; the machine facts `account`, `agent-account` and their
homes keep their names. A machine names an agent account only where the identity seat marks an agent as its agent homes keep their names. A machine names an agent account only where the identity seat marks an agent as its agent
@@ -204,8 +204,8 @@ Checked on 2026-10-17 (playbook 02, step 1); no conflict was found.
## 6. Effects, asks and break-glass (ADR 0387) ## 6. Effects, asks and break-glass (ADR 0387)
Every seat verb and module tool is one permission and carries one **effect**, set in its module's manifest: Every seat verb and module tool is one permission and carries one **effect**, set in its module's manifest:
**reads**, **acts** or **dangerous**. The catalogue's check refuses an unmarked tool, once every module already in **reads**, **acts** or **dangerous**. The catalog's check refuses an unmarked tool, once every module already in
the catalogue is marked. A dangerous tool is granted only by name, and naming one in a role goes to the operator as the catalog is marked. A dangerous tool is granted only by name, and naming one in a role goes to the operator as
an ask first. Every other dangerous act is an ask (ADR 0259), bound to that act and carried out on the warrant. an ask first. Every other dangerous act is an ask (ADR 0259), bound to that act and carried out on the warrant.
The **break-glass** agent holds every tool and never asks. It is sealed until the operator opens it, or a condition The **break-glass** agent holds every tool and never asks. It is sealed until the operator opens it, or a condition
@@ -246,7 +246,7 @@ carry them:
1. A licence token replaced under a running session keeps that session working. 1. A licence token replaced under a running session keeps that session working.
2. The sandbox module loads KVM on the home server, and a sandbox runs there. 2. The sandbox module loads KVM on the home server, and a sandbox runs there.
3. Every existing module's tools carry an effect, and the catalogue's check refuses an unmarked one. 3. Every existing module's tools carry an effect, and the catalog's check refuses an unmarked one.
4. The switch, run on the mesh's own modules in the lab from the bed test kept on the lab's branch 4. The switch, run on the mesh's own modules in the lab from the bed test kept on the lab's branch
`research/053-signed-bus-logins`, loses nothing and leaves every identity on the new bus. `research/053-signed-bus-logins`, loses nothing and leaves every identity on the new bus.
5. The acceptance sentence at the top, end to end. 5. The acceptance sentence at the top, end to end.
@@ -260,7 +260,7 @@ From research 057 (ADR 0411 to ADR 0414):
unaltered and the self-check raises `agent-can-become-root` for it, on a machine that marks no agent account too; unaltered and the self-check raises `agent-can-become-root` for it, on a machine that marks no agent account too;
the operator's existing logins are the operator's existing logins are
unchanged. unchanged.
8. No manifest in the catalogue declares a login for a person or an agent; the catalogue's check refuses one. 8. No manifest in the catalog declares a login for a person or an agent; the catalog's check refuses one.
9. For every machine, the declaration before and after differs, for the modules not set to run for an identity, only 9. For every machine, the declaration before and after differs, for the modules not set to run for an identity, only
in the machine accounts' `user` resources (now the controller's, and `nox` added on the other machines); claude-code on the control node, run for `nox`, places the same in the machine accounts' `user` resources (now the controller's, and `nox` added on the other machines); claude-code on the control node, run for `nox`, places the same
files in the same home; on the other machines the guard stays off and the operator's sessions keep the mesh's tools. files in the same home; on the other machines the guard stays off and the operator's sessions keep the mesh's tools.
@@ -292,7 +292,7 @@ and the issue tracker takes only registered owners. The building waits until the
| # | Outcome, in one sentence | Proves | Repositories and their parts | Depends on | | # | Outcome, in one sentence | Proves | Repositories and their parts | Depends on |
|---|---|---|---|---| |---|---|---|---|---|
| 1 | Every tool on the mesh says whether it reads, acts or is dangerous, a tool's description shows it, and the catalogue refuses a module that leaves one unmarked | test 3 | mesh-sdk: the effect in a tool's declaration, an unmarked tool served as dangerous; mesh-controller: the manifest field; mesh-catalog and mesh-media-catalog: every module marked, the check | — | | 1 | Every tool on the mesh says whether it reads, acts or is dangerous, a tool's description shows it, and the catalog refuses a module that leaves one unmarked | test 3 | mesh-sdk: the effect in a tool's declaration, an unmarked tool served as dangerous; mesh-controller: the manifest field; mesh-catalog and mesh-media-catalog: every module marked, the check | — |
| 2 | A session works on a repository's workspace through the mesh's tools alone: read, glob, search, edit, write, git, its tests and its merge check, with no command runner | — | mesh-catalog: the module `nox-workspace` on the node seat `workspace`, on the always-on nodes; mesh-controller: the seat | 1 | | 2 | A session works on a repository's workspace through the mesh's tools alone: read, glob, search, edit, write, git, its tests and its merge check, with no command runner | — | mesh-catalog: the module `nox-workspace` on the node seat `workspace`, on the always-on nodes; mesh-controller: the seat | 1 |
| 3 | A thread created through the thread seat runs headless on an always-on node, its conversation readable through the seat, its usage counted per thread, and a move to another node loses at most the turn in progress | — | mesh-catalog: `nox-threads` (seat `thread`, restricted streams, leases), `nox-thread-handler` (seat `thread-handler`), claude-code (mirror and rebuild), claude-licence-manager (usage per thread) | 2 | | 3 | A thread created through the thread seat runs headless on an always-on node, its conversation readable through the seat, its usage counted per thread, and a move to another node loses at most the turn in progress | — | mesh-catalog: `nox-threads` (seat `thread`, restricted streams, leases), `nox-thread-handler` (seat `thread-handler`), claude-code (mirror and rebuild), claude-licence-manager (usage per thread) | 2 |
| 4 | The operator starts, follows, answers and stops a thread from a terminal client, and answers its questions on Telegram | — | mesh-catalog: the terminal client, a Go program of `nox-threads`; claude-code: questions and permission requests through the asks | 3 | | 4 | The operator starts, follows, answers and stops a thread from a terminal client, and answers its questions on Telegram | — | mesh-catalog: the terminal client, a Go program of `nox-threads`; claude-code: questions and permission requests through the asks | 3 |
@@ -313,7 +313,7 @@ waits for nothing; 12 waits for milestone 6.
| # | Outcome, in one sentence | Proves | Repositories and their parts (issues) | Depends on | | # | Outcome, in one sentence | Proves | Repositories and their parts (issues) | Depends on |
|---|---|---|---|---| |---|---|---|---|---|
| 11 | The agent's forge account is `nox`, with its keys and its place in the agents' write team kept | test 12 | mesh-catalog gitea: the rename, keys registered on `nox`, every `forge-account` entry (993) | — | | 11 | The agent's forge account is `nox`, with its keys and its place in the agents' write team kept | test 12 | mesh-catalog gitea: the rename, keys registered on `nox`, every `forge-account` entry (993) | — |
| 12 | The mesh makes the operator's logins and nox's on every machine from the identities, and judges nox unable to become root everywhere | tests 6 to 9 | identity module: logins, machines, kept logins, the agent account mark, the operator's verb (994); the node-engine (`mesh-host`): existing logins adopted, every agent login judged (995); mesh-controller: machine accounts composed, node accounts read from the identities, the terminal lines retired (996); mesh-catalog: claude-code declares no login, the catalogue's check refuses one (997) | 6 | | 12 | The mesh makes the operator's logins and nox's on every machine from the identities, and judges nox unable to become root everywhere | tests 6 to 9 | identity module: logins, machines, kept logins, the agent account mark, the operator's verb (994); the node-engine (`mesh-host`): existing logins adopted, every agent login judged (995); mesh-controller: machine accounts composed, node accounts read from the identities, the terminal lines retired (996); mesh-catalog: claude-code declares no login, the catalog's check refuses one (997) | 6 |
| 13 | The operator and nox each reach the bus with their own password, held to their role; the operator's only where no agent runs as the operator | test 11 | mesh-controller: the operator's and nox's bus accounts, secrets at 0600, the fifteen-minute verdict (999); identity module: nox's role refuses approvals and dangerous tools (998); until milestone 7 builds the asks, a dangerous act of nox's is refused, not asked | 12 | | 13 | The operator and nox each reach the bus with their own password, held to their role; the operator's only where no agent runs as the operator | test 11 | mesh-controller: the operator's and nox's bus accounts, secrets at 0600, the fifteen-minute verdict (999); identity module: nox's role refuses approvals and dangerous tools (998); until milestone 7 builds the asks, a dangerous act of nox's is refused, not asked | 12 |
| 14 | The operator starts a claude-code session for nox or for themself, and nox pushes over SSH with its own key | test 10 | mesh-controller: run for, assignments keyed by identity, the refusals (1001); mesh-sdk: calls as the identity, a start goes down only (1002); claude-code: started for nox or for the operator (1003); ssh-client and gitea: nox's own key, closing issue 989 (1004) | 11, 13 | | 14 | The operator starts a claude-code session for nox or for themself, and nox pushes over SSH with its own key | test 10 | mesh-controller: run for, assignments keyed by identity, the refusals (1001); mesh-sdk: calls as the identity, a start goes down only (1002); claude-code: started for nox or for the operator (1003); ssh-client and gitea: nox's own key, closing issue 989 (1004) | 11, 13 |
@@ -0,0 +1,317 @@
---
layer: to-be
status: in-progress
code: [mesh-catalog]
updated: 2026-10-22
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 0472"
- "ADR 0461"
- "ADR 0462"
- "ADR 0278"
- "ADR 0515"
- "ADR 0518"
---
# 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.
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. One list per account, reading and asking to send alike (ADR 0472)
`mesh-mail`'s trusted setting `grants` stays one list of callers per account, giving both reading (that account's
`MAIL` events, `mail_accounts`, `mail_search`, `mail_read`) and asking to send (`mail_send`, `mail_send_status`):
the operator's Approve on every send is the guard (ADR 0472 §1, §2, which supersedes ADR 0460's split; issue 1227
will not be done). `mesh-mail-reader` is named in `grants` for every account after its delivery, on the operator's
Approve, and asks to send nothing: its manifest invokes no send verb (ADR 0472 §3). Who may call `mail-reading`'s
own verbs is its own trusted setting `callers`, `mesh-mail-web` by default (ADR 0459 §2).
## 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 (named in grants) ────────────────┤ 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** (replaced by design [63](63-the-mail-web-ui-with-mail-reading-and-owly.md)'s operator's list, ADR 0515 §5). 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)
**Replaced by design [63](63-the-mail-web-ui-with-mail-reading-and-owly.md):** Owly answers mail questions through
mail reading before threads exist (ADR 0518, changing ADR 0462 §6); the conversation below is what Owly moves onto
once threads exist.
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 named in `grants`
(ADR 0472 §4; 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 asks no `mail_send` for any 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 10's and 11's checks, 13 (with a stand-in router) and 16 are integration tests that start
`mesh-mail-reader` whole against the real bus and store, with stand-ins for the seats it asks (issues 1431, 1524); 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: the seat `mail-reading` declared in `mesh-mail`'s manifest; 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 catalog (issue 806) | 2 |
| 4 | **Replaced by design 63** (2026-10-22): 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 (issue 1419); `mesh-mail`'s folder case and `-any` switches | 2 (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 (its order is said for each): 1426 (the seat `mail-reading` and the module `mesh-mail-reader`), 1517
(its database; after 1426), 1518 (hearing new mail, catching up in arrival order; after 1517), 1519 (its health;
after 1517), 1427 (a batch's question within its bounds; after 1426), 1520 (running batches; after 1427, 1518 and
1519), 1521 (the verbs `entries`, `groups`, `status`; after 1520), 1428 (`correct`, `lessons`, `undo`; after 1521),
1429 (a mail signal's words; after 1427), 1522 (sending it; after 1429 and 1520), 1430 (what the mail page shows;
after 1428, 1521 and 1522), 1523 (corrections and reading lessons on the page; after 1430 and 1428), 1431 (the
reader's integration test; after 1430 and 1522), 1524 (the integration test of failing loudly; after 1431 and
1523), 1418 (`mesh-mail-notifier` unassigned; after 1524, the delivery and test 13 live). Issues 1517 to 1524 were
split from 1426 to 1431 at the prepare step; issue 1227 (the grants split) will not be done (ADR 0472).
- 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
caller; a decision first, after 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 new seat, the new module and the page change ship together, and
`mesh-mail-reader` is named in `grants` 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 is replaced by design [63](63-the-mail-web-ui-with-mail-reading-and-owly.md)**
(2026-10-22): Owly answers mail questions through mail reading before threads exist (ADR 0518), and moves onto a
thread once they do. Issue 1432 will not be done; issue 1433 moves to design 63's milestone 3; issue 1419 stays
open for the move onto a thread.
+214
View File
@@ -0,0 +1,214 @@
---
layer: to-be
status: in-progress
code: [mesh-catalog, mesh-sdk, mesh-controller]
updated: 2026-10-21
decisions:
- "ADR 0464"
- "ADR 0467"
- "ADR 0468"
- "ADR 0474"
- "ADR 0475"
---
# 60 — The brother module
**One module serves the operator's Brother multifunction device: its scanner and its printer are one machine on
the network, so one module, `brother`, holds both seats, `scanner` and `printer`. It scans the glass or the
document feeder into a PDF, keeps every scan in its own bucket and announces it on the bus, prints a document in
one call, reads the device's state and its admin pages, restarts it, and changes the settings on the admin pages
where a mistake cannot lock the operator out or open the device. Only the module's device process ever holds the
device's admin password or the bucket's key.**
(ADR 0464, ADR 0467 and ADR 0468 are in force; ADR 0468 is being built, milestones F1 to F3 below. Proposed, and
not in force: ADR 0475 for the feeder, multi-page and several-document scans; ADR 0474 for
reading every page, configuring, the refused pages and the restart.)
**Acceptance.** The operator, or an agent through the mesh MCP server, scans a document from the glass and finds it
cropped in the scan folder; scans five sheets from the feeder on both sides and finds one PDF of ten pages; prints
that PDF from a file in one call; reads any admin page (ADR 0474, as proposed); changes a setting on an allowed page and is told the old and the new
value; and is refused, with the reason, when asking to change the admin password or the network settings.
## 1. The parts
```
caller (an agent, a module, the operator)
│ scanner.scan · scanner.status · scanner.list · scanner.document
│ printer.status · printer.print · printer.upload · printer.settings
│ printer.configure · printer.restart
▼
brother — tool side (served by the node's tool runner, as the operator account)
│ holds no credential
│ status reads: IPP and eSCL, no login ───────────────────────────▶ the device
│ every other verb: an ask on the work queue device-asks
▼
brother — device process (an account and a bus account of its own)
│ holds the admin password and the bucket's key, and nothing else does
├── eSCL ─────────────────────────────▶ the scanner
├── IPP ─────────────────────────────▶ the printer
├── HTTPS to the pinned certificate ───▶ the admin pages
├── S3 ─────────────────────────────▶ the module's bucket (minio)
└── its answer in the state device-answers ──▶ back to the tool side
```
The two seats are held once for the mesh. A seat names the role, so a later scanner or printer module claims the
same seat and no caller changes (ADR 0467 §2).
The module has two halves. The **tool side** answers the verbs. It runs as the node's operator account, so the
scans it files in the scan folder are the operator's, and it never opens a credential. The **device process** runs
as an account of the module's own with a bus account of its own (ADR 0436 §1), holds no seat and serves no tool. It
alone reads the admin password and the bucket's key, and it alone does the work that needs either. A verb that
needs a credential puts an ask on a work queue and reads the device process's answer back from the module's state,
waiting at most the verb's bound (ADR 0468 §1). The reads that need no credential, the device's state over IPP and
the scanner's over eSCL, stay on the tool side.
The admin password is the operator's own, given once at the operator's desk and kept in the vault; the mesh never
generates or rotates it (ADR 0467 §4). The device process speaks HTTPS to the admin pages and trusts only the
certificate whose SHA-256 fingerprint is pinned in the module's config; any other certificate is refused in the
handshake, before a password is sent (ADR 0467 §3). The module's pin tests check it: a certificate with another
fingerprint is refused and nothing is sent.
## 2. Scanning
What ADR 0464 decided is the glass, one page, one PDF page. ADR 0475 (proposed) widens it to the feeder, to several
documents on the glass and to documents of several scans; until it is accepted, only ADR 0464 holds.
- **The glass.** One page over the whole glass. The document lies on the lid, which is a white of its own, so the
module measures the lid where no document lies and cuts the document out of it with a small margin. When it finds
no document, or one that fills the glass, it keeps the whole page and says it did not crop (ADR 0464 §3).
- **Several documents on the glass** (ADR 0475, proposed). Documents lying apart on the glass are each cut out and
filed as a PDF of their own.
- **One document of several glass scans** (ADR 0475, proposed). A caller starts a document, adds a glass scan to it
as often as it needs, and finishes it, and the pages become one PDF. A document left unfinished is filed whole
after 30 minutes, so no scan is lost.
- **The feeder** (ADR 0475, proposed). The feeder scans whole sheets, one side or both, and every page of the run
goes into one PDF, in the order the scanner gave them: one object in the bucket and one announcement, with its
page count. A feeder page is never cropped. A run that is cut off before its end is an error, never a short PDF
said as success; a unit test checks it.
- **A long run** (ADR 0475, proposed). A feeder run may take longer than a call through the mesh MCP server may
wait. Such a call answers that the scan started, with an id, and the caller reads its outcome by that id. A scan
that was announced is always filed.
- **The real resolution.** The module reads the resolution from the image it receives, never from what it asked: a
scanner may answer a request for 300 dpi with 200 dpi (ADR 0464 §2).
**A lesson for every device the mesh speaks to.** The scanner ignored every scan setting at first: whatever the
request asked, it ran its own default job. The cause was the request's body: the device ignores a body that holds a
line break, and the same request written on one line is honoured. A device's protocol implementation may be
stricter, or stranger, than its standard; when a device ignores a well-formed request, the form of the request
(line breaks, whitespace, the order of fields) is the first suspect, and a capture of what a working client sends
is the evidence.
## 3. Printing
A caller prints in one call: `printer.print` takes exactly one of a key in the bucket (`object`), a file on any
machine of the mesh (`path` with `node`) or a web link (`url`), with the copies, the sides and the paper (ADR 0468
§5). A file on a machine is sent into the bucket by that machine's node-files `upload`, which refuses the secret
places a read refuses, and printed by its key (ADR 0468 §9). A web link is fetched by the device process, at most
60 s, 100 MiB and 5 redirects, and never from an address inside the mesh or a private network (ADR 0468 §6). A
document that is neither PDF nor JPEG is turned into a PDF by the seat `document-converter`, which only-office holds
(ADR 0468 §7). The device process then sends the document to the printer in a format the printer itself lists as
accepted, read before the job.
**No document crosses the bus.** The bus carries a document's key, size, SHA-256 and type; the bytes move over
HTTP between a party and the object store, on a signed link of at most 10 minutes for one object and one method
(ADR 0468 §3). The device process signs links for the module's own steps with its binding's key; every other
party asks `object-store.link`, which minio serves and signs with a key limited to that bucket, for a caller that
brother's `shares` names (ADR 0468 §8). The parts verbs `printer.upload` and `scanner.document` still serve until
issue 1569 removes them.
## 4. The bucket and the events
Every scan is kept once in the module's own bucket under its SHA-256, and announced with the event
`brother.scan.added` once it is stored: its key, size, page count, size in millimetres, resolution and colour mode,
never the image. The scan folder is one consumer of the stored scans: once the scan is stored and announced, the tool side
reads it by a link the device process signed and files it there as the operator; the device process never writes
into the operator's home (ADR 0468 §4). The module deletes nothing from the bucket; the operator removes what they no longer want
(ADR 0468 §2, §4).
The other events are `brother.printer.restarted`, when the device answers again after a restart (ADR 0467 §6), and
`brother.printer.configured`, one for each setting changed (section 5).
## 5. Settings: read and configure
**Reading.** `printer.settings` reads one admin page: the device process logs in, reads the page and logs out. It
answers the page's forms with each field's current value (a radio group's checked button, a checkbox's state, a
select's chosen option) and the values each field allows; the page's links and its hidden fields only on ask. A
password field's value is never given. ADR 0467 §5 decides the read for the pages of the scan and restriction
settings; ADR 0474 (proposed) widens it to every page, and the menu page's links are the map of the rest.
**Configuring** (ADR 0474, proposed). `printer.configure` takes one page and the fields to set on it, by name. The device process logs in
and reads the page as the printer serves it, with a fresh form token. It checks each named field: the field exists
in one form of the page, and its value is allowed for its type (one of a radio group's or a select's values, on or
off for a checkbox, a text within its maximum length). A password, hidden or file field cannot be set. When any
check fails, nothing is sent and the answer names each refused field with what it allows. Otherwise the process
posts the form as the page served it: its own hidden fields, the named fields at their new values, and every other
field at its current value, so nothing the caller did not name changes. It reads the page back, answers each changed
field's old and new value, and says a field the printer did not take as not taken. Then it logs out.
```
caller ──configure {page, set}──▶ tool side ──ask──▶ device process
login ─▶ read page (form, token, current values)
check every named field ──✗──▶ refused, nothing sent
post the whole form ─▶ read back ─▶ logout
◀──answer: old → new per field
brother.printer.configured, one per field
told to the operator: page, fields, who asked
```
**What may not be changed** (ADR 0474, proposed). Some pages are refused, because a mistake on them locks the operator out or opens the
device to the network: the login password, the user restriction and setting lock, the reset menu, the firmware
update and its setup, the security settings, every wired, wireless and Wi-Fi Direct network page, the protocol and
interface pages, and every security page (certificates, TLS, IPsec, the IP and MAC filters). The list is in the
module's code and its tests, not in a config value, so no call can widen it; it is checked against the page asked
and against the address the form posts to; a unit test refuses each listed page. The operator changes such a page in a browser at the device itself.
**Who asked, and who is told** (ADR 0474, proposed). Every change names who asked, the caller the bus proved for the call; a call with no
proven caller is refused. Each changed field is announced as `brother.printer.configured` with the page, the field,
the old and the new value, who asked and when, and the operator is told through the operator channel, once per call.
No password is ever in an answer, an event, a message or a log; a unit test reads each of them for it.
**Restarting.** `printer.restart` is decided by ADR 0467 §5; that it goes through the device's reboot page, posting
that page's own form, is ADR 0474's, proposed. It is the one call that posts to the reboot page, which configure refuses. It is
refused while the device holds a print or scan job, and waits at most five minutes for the printer to answer again
(ADR 0467 §5).
**The module's own defaults stay config.** The scan resolution, colour mode and cropping are the module's config
values, held by the config module and changed through its `config` verb (ADR 0348); they are not trusted values.
Configure changes the device's pages and never the module's config (ADR 0474, proposed), which a unit test checks.
## 6. Health
The device process's health is its unit. The module's health, as ADR 0475 proposes, compares the certificates the
printer serves, without a login, with the pinned one, and is unhealthy, naming the new fingerprint, when the printer
serves one that is not pinned; a printer that does not answer is healthy with a note, and with no printer address set
the module waits for that setting. Until that is accepted, a changed certificate shows only when a verb is refused. A certificate the device
serves that is not pinned is refused, and the refusal names the served and the pinned fingerprint. A device serves
a new certificate after a reset, a firmware update or a certificate's renewal, so a refusal is either that or
another machine answering in its place; only the operator can tell which, and pins the new fingerprint as a config
value. A firmware that moves an admin page is said as a page not found, never as success: a unit test replays a
missing page and expects the error.
## 7. Still open
- **The parts verbs.** `printer.upload` and `scanner.document` carry a document over the bus in parts; issue 1569
removes them once their callers use links, and revises ADR 0475 to ADR 0468.
- **Several printers.** The `printer` and `scanner` seats take one holder; issue 1602 asks for the decision that lets
a second printer join.
- **Restart is not built yet.** `printer.restart` is decided (ADR 0467 §5) and follows `printer.configure`.
- **Not built yet either:** several documents on the glass, a document of several glass scans and the long run's id
(ADR 0475, proposed). The print of a machine's file is ADR 0468's, built in milestone F3.
## Milestones
The milestones build ADR 0468, feature `files-through-the-store`. They are cut by release order: the catalog's
merge check reads manifests with the controller the mesh runs, and the controller vendors the SDK, so a new
manifest field ships SDK first, then the controller and every other strict reader of manifests (the build modules,
the delivery's indexer), then the catalog (playbook 07, *a new manifest word*).
| # | Outcome | Proves | Repository | Issues |
|---|---|---|---|---|
| F1 | A manifest may name who else may read or write its bucket, and every module has one helper that signs and streams a store link. Usable only by F2 and F3. | ADR 0468 §3, §8 (the field) | mesh-sdk | 1591 (`shares` on a requires of s3-bucket), 1592 (link signing and streaming) |
| F2 | The controller accepts `shares` and writes it into the consumer's grant. Usable only by F3. | ADR 0468 §8 | mesh-controller | 1593 (take the SDK); depends on F1 |
| F2b | The build modules read a manifest that names `shares`, so a module carrying it builds. Usable only by F3. | ADR 0468 §8 | mesh-catalog | 1657 (mesh-build-go and mesh-build-node take the SDK); depends on F1; the delivery's indexer takes the same SDK in design 47's own work |
| F3 | The operator prints a stored scan, a file on any machine (a .docx on a laptop among them) or a web link in one call, and scans land in the scan folder from the store; no document these verbs move crosses the bus (the parts verbs stay until issue 1569). Depends on F2 and F2b. | the acceptance above; ADR 0468 §1–§9 | mesh-catalog | minio: 1594 (a signing key per bucket), 1516 (`object-store.link`, retiring the root-signed link); mesh-files: 1597 (upload, download); only-office: 1595 (`document-converter.convert`), 1596 (the converter process); brother: 1598 (print by key or a machine's file), 1599 (print a web link), 1600 (convert before printing), 1601 (the scan folder from the store) |
Order inside F3: minio, then mesh-files and only-office, then brother; one pull request of `mesh-catalog`.
@@ -0,0 +1,75 @@
---
layer: to-be
status: designed
code: [mesh-catalog]
updated: 2026-10-21
decisions:
- "ADR 0479"
- "ADR 0468"
- "ADR 0440"
---
# 61 — The print and scan page
**A web page where the people the operator names sign in with Keycloak, print a PDF or a photo and scan from the glass,
and where only the operator manages the printer. It is a module of its own, `print-and-scan`, that
knows only the seats `printer`, `scanner` and `object-store`, so any module holding the printer and scanner seats
works with it unchanged.** (ADR 0479, accepted 2026-10-21.)
**Acceptance.** A person named in `users`, on a phone that is not a machine of the mesh, signs in, uploads a PDF
and finds it printed, and scans the glass and downloads the PDF; that person is refused the printer's settings and
its restart, which a person named in `managers` may use; a browser that has not signed in sees nothing but the
sign-in.
## 1. The parts
```
a browser (a phone or laptop, inside the mesh or not)
│ HTTPS, through the route proxy's public name, signed in with Keycloak
▼
print-and-scan — renders the pages on the server, holds a bucket of its own
│ printer.status · printer.print · printer.settings · printer.configure · printer.restart
│ scanner.scan · scanner.status · object-store.link
▼
the holders of the printer, scanner and object-store seats ── signed links ──▶ the object store
```
The page claims the seat `print-and-scan`, held once for the mesh, with the verbs `open` and `status`. It calls
only seat verbs, never a module's own tool, and knows no device's protocol (ADR 0479 §1).
## 2. Who may do what
The page requires a Keycloak client, as the mail web client does (ADR 0440 §3). Two trusted settings name the
identities let in, by the ID token's user name: `users` print and scan, `managers` also manage. Both are empty by
default. A browser that has not signed in is sent to the sign-in; an identity in neither setting gets a refusal page
that names no holder, document or setting (ADR 0479 §2, §3).
Its route is public, so a person reaches it from a device that is not a machine of the mesh; this is where it
differs from the mail web client, whose route is internal only (ADR 0479 §3).
## 3. Documents
A document goes from the browser to the page over HTTPS, and from the page into its own bucket by a link the object
store signs; it never crosses the bus, the page keeps none on its disk, an upload is bounded at 100 MiB (refused by its declared length, or cut at the bound, which stores nothing), and a signed link never reaches a browser, because the links name the store's
internal name (ADR 0468 §3, ADR 0479 §4). The page takes PDF and JPEG only, judged by their first bytes; any other
type is refused before anything is stored, and converting other types waits on issue 1650 (ADR 0479 §4). To print,
the page names its own bucket and the key; its bucket shares reading with the printer seat, and the printer's holder
asks the object store for the link itself, a process of the holder matching the seat (ADR 0479 §5). A scan is read from the scanner's bucket, which shares reading with the
page, and streamed to the browser as a download; the page offers the feeder and both sides only once a decision in
force gives them to the scanner seat's scan (ADR 0479 §6).
## 4. Managing the printer
Every user sees the printer's state. Reading and changing its settings and restarting it are the printer seat's
verbs, for managers only (ADR 0479 §7). The printer's holder sees the page as its caller, so the page records the
person: before each such call it announces who asked, what and when, and after it the outcome. Restarting is ADR
0467's, and so is reading the settings, which ADR 0474 proposes to widen; changing them is proposed in ADR 0474,
which is accepted and built before the page (ADR 0479 §9).
## 5. Still open
- **Several printers.** The page serves the seats' one holder; issue 1602 asks for the decision that lets a second
printer join, after which the page lets the user choose (ADR 0479 §8).
- **Other types.** A Word, spreadsheet or web document waits on issue 1650's decision (ADR 0479 §4).
- **Order.** The page is built after design 60's milestones F1 to F3 and after ADR 0474 is accepted and built
(ADR 0479 §9).
@@ -0,0 +1,98 @@
---
layer: to-be
status: proposed
code: [okioki]
updated: 2026-10-21
decisions:
- "ADR 0481"
- "ADR 0440"
- "ADR 0479"
---
# 62 — The bookkeeping portal
**A web page on the private network where the operator, signed in with Keycloak, sees the business's income and
expenses as the bookkeeping service holds them now, and what is still to do per quarter. The `okioki` module that
talks to the service moves, whole, from the laptop to the home server, and the page, `okioki-web`, runs beside it.
Bookkeeping data stays on hardware the operator owns.** (ADR 0481, accepted.)
**Acceptance.** The operator, signed in on a machine of the mesh, opens the page's internal name and sees income and
expenses per month and per quarter, the same period a year before beside them, including an invoice made in the
bookkeeping service minutes before; sees the to-do of a quarter, with a quarter the accountant has submitted shown
as locked; and a browser that has not signed in, or an identity not named in `operators`, sees nothing but the
sign-in or a refusal.
## 1. The parts
```
the operator's browser (a machine of the mesh)
│ HTTP through the route proxy's internal name, signed in with Keycloak
▼
okioki-web — renders the pages on the server, keeps no bookkeeping data
│ over the bus (NATS): bookkeeping.summary · bookkeeping.todo · bookkeeping.documents · bookkeeping.transactions · bookkeeping.document
▼
okioki — the one session with the bookkeeping service, on the home server
│ HTTPS, the service's own web API, as its web app calls it
▼
the bookkeeping service (OkiOki)
```
`okioki` claims the seat `bookkeeping`, held once for the mesh, and serves the verbs the page calls beside the tools
it has (`okioki_todo`, `okioki_link`, …). `okioki-web` claims `bookkeeping-web` with `open` and `status`, and calls
only `bookkeeping` verbs, over the bus, never a module's own tool, as the print and scan page calls only seats (ADR 0479 §1).
`okioki-web` opens no connection to the bookkeeping service and holds none of its secrets or settings: only `okioki`
holds the service's password and session (ADR 0481 §1, §7). Both are Go, and both live in the `okioki` repository.
## 2. Where it runs
Both modules run on the home server, which is always on and is the operator's own hardware (ADR 0481 §1). The
laptop sleeps and travels, so a page there would be away whenever the laptop is. `okioki` runs once: the service
password is sealed on the home server for `okioki` alone, given once at its desk prompt, and the laptop's `okioki` is retired on
the operator's Approve once the home server's answers.
Uploading no longer reads a folder on the laptop. `okioki_upload` takes a scan from the scanner's bucket by its key,
or a mail attachment by its account, message and name, and reads it over the bus (ADR 0481 §3); a scan made on any
machine then reaches the service without being copied between machines.
## 3. Who may see it
The home server's route `reach` is set to `internal` before the page is assigned there, so the route proxy composes
only the internal name, and the page answers 404 to any other host (ADR 0440 §2, ADR 0481 §6). It requires
`oidc-client` from Keycloak, as the mail web client does (ADR 0440 §3). A trusted setting `operators` names the identities let in by the ID token's user name; it is empty by
default, so nobody is let in until the operator names themself. Nothing about the page is public.
## 4. What it shows
- **Overview.** Income (outgoing invoices) and expenses (incoming invoices and receipts, refunds subtracted), excluding
VAT, per month and per quarter, the same period a year before beside it; wages and money in and out of the business
account from the bank, by the operator's own tags. `bookkeeping.summary` computes it in `okioki` from the service's
documents and transactions, so the page does no bookkeeping arithmetic of its own.
- **To-do.** Open payments and documents per quarter, as `okioki_todo` reads them.
- **Documents and transactions.** Searchable lists for a period; a document opens as the file the service keeps,
streamed through `bookkeeping.document` over the bus in numbered parts, as `scanner.document` answers (part, parts,
size, SHA-256, data): `okioki` fetches the file from the service, and the page never follows the service's own link.
The page only reads: it calls no verb that changes the service, and keeps no copy (ADR 0481 §4). Keeping the history in a database of the mesh's own is the own
bookkeeping research (issue 1457), and a decision of its own.
## 5. What the accountant has done is never undone
Every tool of `okioki` that changes the service, whoever calls it, reads the booking or document first and refuses
one the accountant has reviewed, approved for sending or booked, naming it and why, and sends nothing (issue 1608,
ADR 0481 §5). The page shows such items as locked.
## 6. In what order it is built
1. **`okioki` on the home server**: the seat `bookkeeping` with its read verbs, uploads from the scanner's bucket and
from mail, the accountant guard (1608); assigned to the home server, its password given at its desk prompt, the
laptop's retired on the operator's Approve.
2. **The read-only page**: `okioki-web` with the overview, the to-do and the lists, behind Keycloak.
3. **Changes from the page** (linking, tagging, marking paid, closing) are not part of this design: they need a decision
of their own (ADR 0481 §4), and would go through the same guarded tools.
## 7. Still open
- **The route proxy on the home server** did not answer its tools on 2026-10-21; whether it serves an internal name
there is checked before step 2.
- **The guard's fields**: which of the service's fields an accountant's submission sets is read from the service
before 1608 is built.
File diff suppressed because one or more lines are too long
@@ -0,0 +1,213 @@
---
layer: to-be
status: in-progress
code: [mesh-catalog]
updated: 2026-10-22
decisions:
- "ADR 0515"
- "ADR 0516"
- "ADR 0517"
- "ADR 0518"
- "ADR 0519"
- "ADR 0459"
- "ADR 0440"
- "ADR 0461"
- "ADR 0462"
- "ADR 0472"
- "ADR 0378"
- "ADR 0278"
- "ADR 0359"
- "ADR 0361"
---
# 63 — The mail web UI with mail reading and Owly
**The mail page stops being a list of mail and becomes the operator's list: what needs them today, this week and
later, what is promised, what is good to know and what can be read later, each line written by mail reading from
the mail, never copied from it. Beside it sits Owly, the mail page's assistant, which answers questions about the
mail, writes drafts the operator sends on their Approve, and suggests tidying the mailbox into an approval inbox,
where only the operator's Approve changes anything. Every action of the operator on the page is recorded, so mail
reading and Owly judge better over time. The classical list stays one click away, as All mail.**
(ADR 0515 to ADR 0519, building on design 59 and ADR 0459.)
The page has no name of its own: it is `mesh-mail-web`'s page, the mail page. Its reference is the mockup the
operator picked on 2026-10-22, kept beside this design as [`63-mockup/mail-page.html`](63-mockup/mail-page.html), a
direction and not a pixel specification; its mail is invented, and no name, address, employer, domain, provider
or amount in it is the operator's.
## 1. What it builds on
Design [59](59-the-mail-web-client.md) delivered the mail web client (its milestone 1) and mail reading (its
milestone 2): `mesh-mail-web` renders the mail page on the server, and `mesh-mail-reader` reads each new mail into
a mail entry, sends a mail signal for the mail that matters and keeps the operator's corrections as reading
lessons. What runs today is the as-is [18](../00-as-is/18-the-mail-modules.md). The operator judged that page
unusable on 2026-10-22: it shows mail groups over a classical list, so the operator still reads each mail to know
what it asks.
This design replaces design 59's milestone 4 (the mail conversation on a thread) and its page of mail groups:
Owly answers before threads exist (ADR 0518) and moves onto a thread once they do, and the page opens on the
operator's list (ADR 0515 §5). It is a design of its own, not an amendment of design 59, by the operator's choice
on 2026-10-22: design 59 is the mail web client and mail reading as first built and closes with its own last steps
(test 13 live, issue 1418, and Betterbird's removal, issue 806); this one is the page rebuilt from a visual design
(playbook [10](../../00-META/process/10-visual-design.md)), in six milestones of its own. Design 59 says where it is
replaced.
## 2. The operator's list (ADR 0515, ADR 0516)
- **Mail reading notes what each mail asks.** A mail entry also holds at most 3 tasks (what to do, and by when),
at most 3 promises (*you promised*, *waiting on others*, *on its way*, with who and when) and whether the mail is
*read later*. Each line is the model's own sentence, at most 120 characters, and never quotes the mail. A batch
holds at most 6 mails, so its larger answer stays within 40 s (ADR 0516).
- **The page opens on the operator's list.** A greeting says what mail signals told the operator today; then
*today*, *this week* and *later* (tasks by their due date), *you promised*, *waiting on others*, *on its way*,
*good to know*, *read later*, *handled today* with undo, and the scam warnings in their own place (ADR 0515 §1).
- **Modes choose accounts.** *Work*, *Personal* and *Everything* each show the accounts `mesh-mail-web`'s setting
`modes` names (ADR 0515 §4).
- **Phone first.** One column on a phone; a wide screen adds the reading pane, and Owly beside the operator's list (ADR 0515
§2, §3).
- **All mail is the classical list.** Accounts, folders, search and one mail stay a view the operator can always
open; a correction of mail reading is made from the reading pane (ADR 0515 §5).
- **One script and one stylesheet from its own origin.** The page stays rendered on the server; the script makes
handling, undo and Owly work without a reload and talks only to the page's own origin. A mail's body stays
escaped text, and an attachment stays a download until milestone 2's decision (ADR 0515 §6).
- **The operator's list starts full.** After the delivery, mail reading reads the last 14 days of inbox mail once more
(ADR 0516 §5).
## 3. Handling and the action log (ADR 0517)
- **Handled is the operator's.** Ticking a task, a promise or a mail handled hides it under *handled today*, with
an undo until the day ends; *remind me tomorrow* hides it until the next day. Nothing in the mailbox changes.
- **Every action is a line in the action log:** handled, undone, reminded, opened, corrected, asked, approved or
*not now*, the mode chosen, and the page opened (*last looked*, which gives *new since you last looked*). A line
holds the act, the mail by its id, the mode and when, and a question's own words; never mail text. Kept a year.
- **The log teaches.** Per sender over 90 days, the counts of mails, opened, handled unopened, reminded and
corrected reach each batch and each question in a part of their own, at most 4 KiB (milestone 5).
- **The verbs.** `mail-reading` gains `list`, `handle`, `unhandle`, `remind`, `record` and `actions`; the page
calls them for the signed-in operator and keeps no state of its own.
## 4. Owly answers (ADR 0518)
- **`ask` on `mail-reading`.** A question (at most 2 KiB) is answered in two judgements: the first chooses up to 5
mails and 3 search terms from the mail entries; mail reading calls `mail_search` and `mail_read`; the second
answers from those mails and names only mails it was given. A question the mails do not answer is answered so.
- **On another node.** Owly's judgements run on the node `ask-node` names (the workstation by default), never on
the node of the batches and the issue tracker, so ADR 0459 §15's spacing holds unchanged. An answer takes up to
about 90 s, 130 s behind the morning briefing; the page shows how long Owly has been waiting.
- **The morning briefing** is one judgement a day from the last 7 days' mail entries, read aloud by the browser's
own speech on the operator's click.
- **Nothing of the conversation is kept** but the question, in the action log. A follow-up is answered fresh until
Owly moves onto a thread (issue 880, then issue 1419 for the thread's caller).
- **On the page:** Owly is a panel beside the operator's list on a wide screen and a sheet on a phone, with suggestion
chips for the questions the operator's list makes likely.
## 5. Owly writes (milestone 4)
**Proposed scope, not yet decided.** This section is what the mockup shows; its decision is written in
milestone 4's prepare step, before any of it is built, and the section is then rewritten to it.
Owly offers three short reply options for a mail that asks a question, a draft in the mail's own language, a new
mail from a sentence ("mail Lena that I'll pick the photos tomorrow"), a nudge for a promise the operator waits
on, and an answer to an invitation. Every one is the operator's to change, and goes out only through `mail_send`
on the operator's Approve on Telegram (ADR 0440 §6, ADR 0361 §2). Drafting is outside every decision so far (ADR
0462 §7 leaves it to issue 1259), and a reply needs `In-Reply-To` and cc (issue 1257): the decision is written in
milestone 4's prepare step, tracked by issue 1864.
## 6. Owly learns and tidies (ADR 0519, ADR 0517 §6)
- **Suggested actions.** Mail reading may suggest *archive*, *report as phishing* or *unsubscribe*, with why; it
carries none out. Batches suggest from scam warnings and from senders the operator never opens; Owly suggests
when asked in words.
- **The approval inbox** on the page shows each, with *Approve* and *Not now*. The signed-in operator's Approve is
the decision; `mesh-mail-web` then calls `mail-store`'s new verb. *Not now* keeps a sender quiet for 30 days.
- **mail-store's writes:** `archive`, `report`, `restore` (the same day's undo) and `unsubscribe`, by the mail's own
one-click way (RFC 8058, to a public address only) or as a mail send on the operator's Approve. `mesh-mail`
answers them only for the mail web client, which calls them only from the page's Approve and undo forms, so nothing
else on the mesh can change the mailbox; an undo writes an *undone* line through `decide`. Flag, delete and draft stay with issue 730.
- **The action log's counts reach the batches** and Owly's questions, in a part of their own (ADR 0517 §6 to §8).
- **Teaching in the chat (proposed scope, not yet decided).** "Stop telling me about Strava" becomes a reading
lesson, shown under *Owly learned* with an undo. Whether a lesson taught in words needs more than ADR 0459 §8
decides is settled in milestone 5's prepare step, tracked by issue 1866, before it is built. *Remind me tomorrow*
and *read later* are milestone 1's (ADR 0516, ADR 0517).
## 7. Fast on the desktop (milestone 6)
**Proposed scope, not yet decided:** its decision is written in milestone 6's prepare step, tracked by issue
1867, before it is built.
Keyboard shortcuts (`j` and `k` move, `x` ticks, `r` opens the reply options, `/` jumps to Owly, `?` lists them)
and bulk actions (tick several, handle or archive them at once, each archive still on the approval inbox's
Approve). Tracked by issue 1867.
## 8. What stays outside
- **A calendar.** "Put the viewing in my calendar" needs a calendar on the mesh; none exists, so the page shows the
date and offers nothing more. No issue is opened until a calendar is decided.
- **Attachments shown in place** change ADR 0440 §5 (an attachment is only a download); milestone 2 writes that
decision before it builds (issue 1861).
- **The mail conversation on a thread** waits for threads (issue 880) and its caller (issue 1419); ADR 0462 then
decides again for Owly.
- **The work account** stays a later decision of its own (ADR 0359 §7).
## 9. The build's acceptance tests
1. Each new mail's entry holds its tasks, promises and read-later mark; every line is at most 120 characters and
shares no run of 8 words with the mail's text; a batch holds at most 6 mails and its question stays under 64
KiB (ADR 0516 §1 to §3).
2. A correction removing a task or a promise, or changing the read-later mark, is kept as a reading lesson and
followed by the next batch (ADR 0516 §4).
3. After the delivery the last 14 days of inbox mail are read once, and a restart does not read them again
(ADR 0516 §5).
4. The mail page opens on the operator's list: the greeting with today's mail signals, the sections in their order, scam
warnings in their own place, the modes from `modes`, and *All mail* opening the classical list (ADR 0515).
5. Handling a task moves it to *handled today*, undo returns it, remind hides it until the next day, and each act
leaves one line in `actions` with no mail text; a line older than 365 days is removed (ADR 0517).
6. Every page's content security policy allows script and style from its own origin only, no inline script, and
`connect-src 'self'`; a mail body with `<script>` and a remote image is shown as text (ADR 0515 §6).
7. Tests 1, 2, 4 and 5 run in one integration test that starts `mesh-mail-reader` and `mesh-mail-web` whole
against the real bus and store, with a fixed judgement (issue 1860).
8. Live: the operator uses the operator's list on the phone and on the desktop for a day.
9. (Milestone 2, proposed until its decision.) A photo, a PDF's pages, an invitation, a sheet and a zip's contents open in place, as its decision
says (issue 1861).
10. (Milestone 3.) `ask` answers naming only mails it was given, says so when nothing answers, never judges on the
batches' node, and records the question with no answer text; the morning briefing is one judgement a day from
the entries alone (ADR 0518).
11. (Milestone 3.) Live: the operator asks three questions on the phone and the desktop, and each answer names
real mails.
12. (Milestone 4, proposed until its decision.) A reply option, a draft and a nudge each reach the operator's Approve on Telegram, and nothing
is sent without it (issue 1864).
13. (Milestone 5.) A suggestion appears in the approval inbox; Approve archives the mail, undo restores it,
*Not now* is recorded and not suggested again for 30 days; a one-click unsubscribe refuses a private address;
`mesh-mail-reader` invokes no write verb (ADR 0519).
14. (Milestone 5.) The per-sender counts reach a batch's question in a part of their own of at most 4 KiB, the
batch's senders first, and the reading lessons keep their 12 KiB (ADR 0517 §6); a lesson taught in the chat
changes the next batch and is undone from *Owly learned* (proposed until its decision).
15. (Milestone 6, proposed until its decision.) Each shortcut does what section 7 says, and a bulk handle of five tasks is five lines in the
action log.
## 10. Milestones
| # | Outcome, in one sentence | Proves | Repositories and their parts | Depends on |
|---|---|---|---|---|
| 1 | The operator opens the mail page on the phone or the desktop and sees what needs them, ticks it off, undoes it, and switches between Work and Personal | tests 1 to 8 | mesh-catalog: `mesh-mail-reader`'s tasks, promises and read later (1857), handling and the action log (1858); `mesh-mail-web`'s operator's list (1859, with 1850); the integration test (1860); one entry per message (1849), the health's first state (1580) | — |
| 2 | Photos, PDFs, invitations, sheets and zips open in place on the mail page | test 9 | mesh-catalog: `mesh-mail`'s previews and `mesh-mail-web`'s viewer (1861), after its decision | 1 |
| 3 | The operator asks Owly about their mail and hears a morning briefing | tests 10, 11 | mesh-catalog: `mail_search` made fit (1433); `mail-reading`'s `ask` (1862); Owly on the page (1863) | 1 |
| 4 | Owly writes reply options, drafts, new mail and nudges, each sent on the operator's Approve | test 12 | mesh-catalog: drafting and replies (1864), after its decision | 3 |
| 5 | Owly suggests tidying into an approval inbox, and learns from what the operator teaches and does | tests 13, 14 | mesh-catalog: `mail-store`'s writes and the approval inbox (1865, answering 730 for its four verbs); teaching and the counts (1866) | 3 |
| 6 | On the desktop the operator works the operator's list from the keyboard and handles several items at once | test 15 | mesh-catalog: shortcuts and bulk actions (1867) | 1 |
**Milestones 1 to 6 and their issues, confirmed by the operator on 2026-10-22.** Each milestone is one release of
`mesh-catalog` (playbook [07](../../00-META/process/07-feature-branches.md)), built one at a time, the smallest useful
one first. Their issues, in their order (each depends on the one before unless said):
- Milestone 1: 1857 (tasks, promises, read later), 1858 (handling and the action log; after 1857), 1859 (the operator's list
on the page, with 1850; after 1858), 1860 (the integration test; after 1859); 1849 and 1580 depend on none.
- Milestone 2: 1861 (a decision first, ADR 0440 §5).
- Milestone 3: 1433 (depends on none), 1862 (`ask`; after 1857), 1863 (Owly on the page; after 1862 and 1859).
- Milestone 4: 1864 (a decision first; after 1863).
- Milestone 5: 1865 (after 1863; answers 730 for archive, report, restore and unsubscribe), 1866 (after 1857, 1858 and
1863).
- Milestone 6: 1867 (after 1859).
Issue 1432 (design 59's mail conversation) will not be done: Owly replaces it (ADR 0518). Issue 1419 stays open
for the move onto a thread, which ADR 0518 leaves to threads; Owly's first version reads as `mesh-mail-reader`
and does not need it.
+6 -1
View File
@@ -48,7 +48,7 @@ document is written and this one's status becomes `implemented`.
| [`45-a-core-that-cannot-fail-silently.md`](45-a-core-that-cannot-fail-silently.md) | **Designed.** The core says when it is wrong, refuses what is stale or unreadable, heals what it knows, upgrades one machine at a time with a witness that rolls it back, and is checked against the real mesh before merge: the writers and signals tables, the condition store, `doctor`, the minimal output channel, healers, the lease and report order, staged upgrades, the facts snapshot and replays, in six phases | ADR 0227, ADR 0224, ADR 0218 | | [`45-a-core-that-cannot-fail-silently.md`](45-a-core-that-cannot-fail-silently.md) | **Designed.** The core says when it is wrong, refuses what is stale or unreadable, heals what it knows, upgrades one machine at a time with a witness that rolls it back, and is checked against the real mesh before merge: the writers and signals tables, the condition store, `doctor`, the minimal output channel, healers, the lease and report order, staged upgrades, the facts snapshot and replays, in six phases | ADR 0227, ADR 0224, ADR 0218 |
| [`46-the-conversation-with-the-operator.md`](46-the-conversation-with-the-operator.md) | **Designed.** The mesh tells and asks its operator over channels that are holders of two kinded benches, `channel` and `intake`, declaring capabilities from a fixed vocabulary; the router orders them by work context and never lowers the bar; an answer that performs an action is checked and performed by the controller, on a TOTP code or a verified Telegram sender, never on a desk click alone; operator messages addressed or answered by the mesh's responder, untrusted input kept as data, references for what words may not carry, and a recoverable factor; Telegram first, in six phases | ADR 0234, ADR 0227 | | [`46-the-conversation-with-the-operator.md`](46-the-conversation-with-the-operator.md) | **Designed.** The mesh tells and asks its operator over channels that are holders of two kinded benches, `channel` and `intake`, declaring capabilities from a fixed vocabulary; the router orders them by work context and never lowers the bar; an answer that performs an action is checked and performed by the controller, on a TOTP code or a verified Telegram sender, never on a desk click alone; operator messages addressed or answered by the mesh's responder, untrusted input kept as data, references for what words may not carry, and a recoverable factor; Telegram first, in six phases | ADR 0234, ADR 0227 |
| [`47-delivery-from-commit-to-delivered.md`](47-delivery-from-commit-to-delivered.md) | **Designed.** A delivery is one commit in one repository, from its pull request's head to every machine; a delivery group is deliveries sharing a branch name, ordered and checked as one future state; both owned by the `mesh-delivery` module with one state table, its state on the bus, every transition said, noted on the commit and shown on the pull request; the controller keeps the planner, the gate, sending and the rollout, and the core's own updates never wait for the module | ADR 0239, ADR 0238, ADR 0236 | | [`47-delivery-from-commit-to-delivered.md`](47-delivery-from-commit-to-delivered.md) | **Designed.** A delivery is one commit in one repository, from its pull request's head to every machine; a delivery group is deliveries sharing a branch name, ordered and checked as one future state; both owned by the `mesh-delivery` module with one state table, its state on the bus, every transition said, noted on the commit and shown on the pull request; the controller keeps the planner, the gate, sending and the rollout, and the core's own updates never wait for the module | ADR 0239, ADR 0238, ADR 0236 |
| [`49-the-mesh-in-domains.md`](49-the-mesh-in-domains.md) | **In progress.** Every concept belongs to one of eleven domains, which owns its one word; the glossary is organised by them and is the authority, a retired word is named on its replacement's line with its scope, and two checks hold this repository's documents and the catalogue's tool descriptions to it | ADR 0244, ADR 0006, ADR 0008, ADR 0302 | | [`49-the-mesh-in-domains.md`](49-the-mesh-in-domains.md) | **In progress.** Every concept belongs to one of eleven domains, which owns its one word; the glossary is organised by them and is the authority, a retired word is named on its replacement's line with its scope, and two checks hold this repository's documents and the catalog's tool descriptions to it | ADR 0244, ADR 0006, ADR 0008, ADR 0302 |
| [`50-split-dns-on-a-machine-with-a-vpn-client.md`](50-split-dns-on-a-machine-with-a-vpn-client.md) | **In progress.** A machine whose VPN client pushes servers of its own runs a resolver of its own, on a node seat, that routes the VPN's domains over its link and every other name to the mesh's resolvers; the VPN client's module carries the adapter | ADR 0247, ADR 0223 | | [`50-split-dns-on-a-machine-with-a-vpn-client.md`](50-split-dns-on-a-machine-with-a-vpn-client.md) | **In progress.** A machine whose VPN client pushes servers of its own runs a resolver of its own, on a node seat, that routes the VPN's domains over its link and every other name to the mesh's resolvers; the VPN client's module carries the adapter | ADR 0247, ADR 0223 |
| [`51-the-registries-keep-what-is-named.md`](51-the-registries-keep-what-is-named.md) | **In progress.** The artifact store, the package registry and each machine's container runtime get tools that say what they hold, what names each thing and what could go; every removal is a dry run unless a person asks and says why, and what decides is always a record | ADR 0251, ADR 0189 | | [`51-the-registries-keep-what-is-named.md`](51-the-registries-keep-what-is-named.md) | **In progress.** The artifact store, the package registry and each machine's container runtime get tools that say what they hold, what names each thing and what could go; every removal is a dry run unless a person asks and says why, and what decides is always a record | ADR 0251, ADR 0189 |
| [`52-shares-and-mounts.md`](52-shares-and-mounts.md) | **In progress.** A node shares one folder at a time through `nfs-server`, which provides it as a provision to the private network only, mapping every client to the folder's owner; a node that asks mounts it through `mounts`, a mount unit and an automount per share, so nothing mounts or fails at boot; a device that comes and goes is an occasional source whose absence is a state | ADR 0263, ADR 0051, ADR 0092 | | [`52-shares-and-mounts.md`](52-shares-and-mounts.md) | **In progress.** A node shares one folder at a time through `nfs-server`, which provides it as a provision to the private network only, mapping every client to the folder's owner; a node that asks mounts it through `mounts`, a mount unit and an automount per share, so nothing mounts or fails at boot; a device that comes and goes is an occasional source whose absence is a state | ADR 0263, ADR 0051, ADR 0092 |
@@ -58,6 +58,11 @@ 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 | | [`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 | | [`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 | | [`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 |
| [`60-the-brother-module.md`](60-the-brother-module.md) | **In progress.** One module, `brother`, holds the `scanner` and `printer` seats for the Brother multifunction device: scans from the glass (cropped) or the feeder (one PDF), keeps each scan in its bucket and announces it, prints in one call, reads the admin pages and changes settings on those where a mistake cannot lock the operator out, each change recorded and told; only its device process holds the credentials | ADR 0467, ADR 0464, ADR 0468, ADR 0474 |
| [`61-the-print-and-scan-page.md`](61-the-print-and-scan-page.md) | **Designed.** A web page, behind a Keycloak sign-in and on a public name, where the people the operator names print a PDF or a photo and scan the glass, and only managers manage the printer: a module of its own, `print-and-scan`, that calls only the printer, scanner and object-store seats, so any holder of those seats works with it; documents go from the browser to the page and by signed links to the store, never over the bus | ADR 0479, ADR 0468, ADR 0440 |
| [`62-the-bookkeeping-portal.md`](62-the-bookkeeping-portal.md) | **Proposed.** A web page on the private network, behind the operator's Keycloak sign-in, showing the business's income, expenses and to-do as the bookkeeping service holds them now; `okioki` moves whole to the home server and the page `okioki-web` runs beside it, calling only the `bookkeeping` seat; nothing the accountant reviewed or submitted is ever changed | ADR 0481, ADR 0440, ADR 0479 |
| [`63-the-mail-web-ui-with-mail-reading-and-owly.md`](63-the-mail-web-ui-with-mail-reading-and-owly.md) | **In progress.** The mail page becomes the operator's list: what needs them today, this week and later, what is promised, what is good to know and what can be read later, written by mail reading; Owly beside it answers, writes drafts sent on the operator's Approve, and suggests tidying into an approval inbox; every action on the page is recorded so reading judges better; in six milestones | ADR 0515, ADR 0516, ADR 0517, ADR 0518, ADR 0519 |
## Not yet written ## Not yet written
@@ -1,8 +1,8 @@
# Diagnosis — 2026-09-21 # Diagnosis — 2026-09-21
1. The report's own remedy was taken: the same order against a second ACME implementation. The 1. The report's own remedy was taken: the same order against a second ACME implementation. The
catalogue's own certificate authority, step-ca, was raised in the bed beside Pebble, pinned as catalog's own certificate authority, step-ca, was raised in the bed beside Pebble, pinned as
the catalogue pins it, and the same proxy pointed at it over the same challenge path. It issued the catalog pins it, and the same proxy pointed at it over the same challenge path. It issued
within a second of the order. within a second of the order.
2. The name was still not served, and the reason was neither authority's: the bed's stand-in 2. The name was still not served, and the reason was neither authority's: the bed's stand-in
backend, a netcat loop behind the route, used flags this machine's netcat has not got and never backend, a netcat loop behind the route, used flags this machine's netcat has not got and never
@@ -1,6 +1,6 @@
# Diagnosis — 2026-09-21 # Diagnosis — 2026-09-21
1. The catalogue was read again for mounts no resource declares: twenty-four remained, in two 1. The catalog was read again for mounts no resource declares: twenty-four remained, in two
kinds only. Nine media modules mount the operator's library, and every one of those paths is kinds only. Nine media modules mount the operator's library, and every one of those paths is
already in the module's `accesses` — the vocabulary ADR 0051 already in the module's `accesses` — the vocabulary ADR 0051
gave exactly this. Four modules mount the container runtime's socket, and every one of them gave exactly this. Four modules mount the container runtime's socket, and every one of them
@@ -9,6 +9,6 @@
check that was withdrawn needed only to read both: an access is a declared path, and a check that was withdrawn needed only to read both: an access is a declared path, and a
capability declares the facility it grants. capability declares the facility it grants.
**Located in:** the catalogue's parser. The check is back, refuses an undeclared mount naming the **Located in:** the catalog's parser. The check is back, refuses an undeclared mount naming the
path and the three remedies, and a test parses the whole catalogue. Decided in path and the three remedies, and a test parses the whole catalog. Decided in
ADR 0091. ADR 0091.
@@ -18,7 +18,7 @@ Not "a registry cannot be installed" — **a mesh cannot hold its own modules.**
The artifact store is one store for everything a module ships: images, and archives, which are The artifact store is one store for everything a module ships: images, and archives, which are
directories from a module's repository packed and pushed as content-addressed blobs. So it is the directories from a module's repository packed and pushed as content-addressed blobs. So it is the
mesh's module catalogue in artefact form, and the same role a separate object store plays in the mesh's module catalog in artefact form, and the same role a separate object store plays in the
arrangement being replaced. arrangement being replaced.
Until it exists, a module can be described and resolved but nothing it carries can be kept Until it exists, a module can be described and resolved but nothing it carries can be kept
@@ -6,7 +6,7 @@ opened: 2026-09-04
## What was observed ## What was observed
Rolling the module runtime out to the catalogue (the runtime that serves a module's tools and Rolling the module runtime out to the catalog (the runtime that serves a module's tools and
runs its events under the module's own account), each tools+events module receives its runs its events under the module's own account), each tools+events module receives its
configuration the way the design intends: a mergeable config file the module declares, into configuration the way the design intends: a mergeable config file the module declares, into
which the assignment's settings are merged. The runtime container mounts that file and reads which the assignment's settings are merged. The runtime container mounts that file and reads
@@ -38,7 +38,7 @@ failing to load because the value that would fix them is present but unread.
## Why it matters beyond this instance ## Why it matters beyond this instance
Every tools+events module converted to the runtime model now takes its URL and credentials Every tools+events module converted to the runtime model now takes its URL and credentials
this way, so this is not one module's quirk — it is the config path for the whole catalogue. this way, so this is not one module's quirk — it is the config path for the whole catalog.
The gap turns the headline promise of the settings design ("change it without editing it, on The gap turns the headline promise of the settings design ("change it without editing it, on
the fly") into "change it, then recreate the container by hand," which is the manual step the the fly") into "change it, then recreate the container by hand," which is the manual step the
design existed to remove. And because the file is genuinely updated, nothing surfaces the design existed to remove. And because the file is genuinely updated, nothing surfaces the
@@ -6,7 +6,7 @@ opened: 2026-09-02
## The symptom, as observed ## The symptom, as observed
Found by review of the catalogue examples (2026-09-02), not by an outage — the outage is the Found by review of the catalog examples (2026-09-02), not by an outage — the outage is the
part the design permits to be silent. part the design permits to be silent.
The cache module declares its access-control file as an ordinary file resource with fixed, The cache module declares its access-control file as an ordinary file resource with fixed,
@@ -40,5 +40,5 @@ even when what grew inside since is somebody else's work the mesh asked for.
some other way? some other way?
- ADR 0010 treats every added resource type as a security artefact. Does a create-once - ADR 0010 treats every added resource type as a security artefact. Does a create-once
semantic widen what a compromised control plane can express, or narrow it? semantic widen what a compromised control plane can express, or narrow it?
- Are there other seeded-then-mutated files already in the catalogue that this failure is - Are there other seeded-then-mutated files already in the catalog that this failure is
waiting inside? waiting inside?
@@ -6,7 +6,7 @@ opened: 2026-09-02
## The symptom, as observed ## The symptom, as observed
Found by review of the catalogue examples (2026-09-02). The media stack is several modules — Found by review of the catalog examples (2026-09-02). The media stack is several modules —
a library server, the acquisition managers, a download client and their satellites — and each a library server, the acquisition managers, a download client and their satellites — and each
of them declares the same library and download directories as its own resources. of them declares the same library and download directories as its own resources.
@@ -28,7 +28,7 @@ shared workspace of several modules was written six times as six private ones. T
a media-stack peculiarity: any pipeline of modules handing files to each other on one machine a media-stack peculiarity: any pipeline of modules handing files to each other on one machine
(an ingest directory, a spool, a drop folder) hits the same wall. (an ingest directory, a spool, a drop folder) hits the same wall.
It is also a fork in the design the catalogue has otherwise avoided: the fix could be a new It is also a fork in the design the catalog has otherwise avoided: the fix could be a new
owning module the others depend on, a shared-resource concept in the manifest, or a statement owning module the others depend on, a shared-resource concept in the manifest, or a statement
that co-located file handoff is not a thing the mesh supports and these modules are one that co-located file handoff is not a thing the mesh supports and these modules are one
module. Each answer changes what a module *is*, which is why this is an issue and not a patch. module. Each answer changes what a module *is*, which is why this is an issue and not a patch.
@@ -6,7 +6,7 @@ opened: 2026-09-05
## The symptom, as observed ## The symptom, as observed
Found while converting the catalogue (2026-09-05), across several modules at once. A module can Found while converting the catalog (2026-09-05), across several modules at once. A module can
declare *things that exist* — a directory, a file with fixed content, a network, a container — but declare *things that exist* — a directory, a file with fixed content, a network, a container — but
it cannot declare *a step that runs* at a defined point in its own lifecycle. Three converted it cannot declare *a step that runs* at a defined point in its own lifecycle. Three converted
modules need exactly that and have nowhere to put it: modules need exactly that and have nowhere to put it:

Some files were not shown because too many files have changed in this diff Show More