Compare commits

..
Author SHA1 Message Date
jschoubben 27dbfd763b Answer A-36: mesh-sdk's calls words and the TS follower, as built
Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 22:06:12 +01:00
jschoubben d0a7565088 Answer A-33 to A-35: the gate's records from take-in, source, owed acts
mesh/delivery merged: source.main moved to novox/hq@2b731e11414c
mesh/check checking d0a7565088f9 on 2b731e11414c
Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 22:05:00 +01:00
jschoubben d60f14c56c Answer A-30 to A-32: declared finding events, old keys, held writers
Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 22:03:55 +01:00
jschoubben 2103f4479b Answer A-29: one mesh-sdk writer that observes from its own state
Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 22:01:05 +01:00
jschoubben a1c77688ab A-23 names the trace operation, as built
Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 22:00:25 +01:00
jschoubben dca4bc973e Answer A-28: own followers through a reader, a single ask through the writer
Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 22:00:05 +01:00
jschoubben 201e998418 Answer A-27: the tool runner's first views are adopted by a process reader
Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 21:59:52 +01:00
mesh-tools 597d097982 Merge pull request 'Design 47 G: the lead's answers A-16 to A-25, each tested against the principles' (#554) from design/47-G-lead-answers-2 into main 2026-11-11 20:58:51 +00:00
jschoubben a1efa45250 Correct A-17: mesh-sdk names no seat; the module passes the contract's findings-asked subject
mesh/delivery merged: source.main moved to novox/hq@597d0979828f
mesh/check checking a1efa452500f on 597d0979828f
Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 21:57:51 +01:00
jschoubben c0409d137a Answer A-26: followers' findings are the reader's, through Adopt
Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 21:57:32 +01:00
jschoubben 0bcfff8e11 Correct A-18: checks goes with the merge states, as §431 says
mesh/delivery closed: a newer head of the same pull request
mesh/check checking 0bcfff8e1189 on 269d226a8d14
Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 21:55:34 +01:00
jschoubben 9e3b63a6db Answer A-25: a session put keeps its description
mesh/delivery closed: a newer head of the same pull request
mesh/check checking 9e3b63a6dbfc on 269d226a8d14
Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 21:55:12 +01:00
jschoubben cab8c547f1 Correct A-13: findings-asked is heard on the seat's subject, not the part's stream
Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 21:55:01 +01:00
jschoubben d0597bd2e5 Answer A-24: the gate's reading verbs answer the delivery records
Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 21:54:44 +01:00
jschoubben 21fa15d96d Answer A-23: the gate's recovery mark is one field of the record
Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 21:54:44 +01:00
jschoubben 1ac0f4e311 Extend A-22: resolved words are dropped in every module
Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 21:54:44 +01:00
jschoubben 4d729a009a Answer A-22: the self-check and healers moves, as built
Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 21:54:44 +01:00
jschoubben c3de071fc8 Answer A-21: the object sessions' store, chunk and threshold
Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 21:54:44 +01:00
jschoubben b057329e65 Answer A-20: the gate's kept data moves with the fewest parts that lose none
Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 21:54:44 +01:00
jschoubben 99d9721ff3 Answer A-19: HearSeatEvents' read at the start
Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 21:54:44 +01:00
jschoubben c58a00b3a7 Answer A-18: what G retires in the gate leaves no trace
Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 21:54:44 +01:00
jschoubben 7c476bfb4a Answer A-17: the writer names findings-asked with its seat
Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 21:54:44 +01:00
jschoubben 7dc0707008 Answer A-16: one mesh-sdk constructor makes a stdio module's finding writer
Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 21:54:44 +01:00
mesh-tools 269d226a8d Merge pull request 'Design 47 G: answer develop's questions against the principles; the outside-cause wait whole (G-19); lead answers (G-20)' (#553) from design/47-G-develop-answers-7 into main 2026-11-11 20:54:19 +00:00
jschoubben b6556a4bcf Answer A-15: the branch follower's not-found, as built
mesh/delivery merged: source.main moved to novox/hq@269d226a8d14
mesh/check being carried to b6556a4bcfac on 269d226a8d14 | from 60e99f38759b
Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 21:41:47 +01:00
jschoubben 0e1a742b10 Answer A-10 to A-14 against the principles: seat events, the granting wait's release, the writer's subjects, ref in the id
mesh/delivery closed: a newer head of the same pull request
mesh/check checking 0e1a742b1066 on 60e99f38759b
Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 21:40:29 +01:00
jschoubben cfb6666711 Answer A-4 to A-9 against the principles: grants, the branch follower, the gate's kept data, take-in bodies
mesh/delivery closed: a newer head of the same pull request
mesh/check pass cfb66667112f on 60e99f38759b | unit 43 run, 0 kept, 1 skipped | integration 43 run, 0 kept, 1 skipped | composition passed in 51s on 4…
Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 21:36:23 +01:00
jschoubben 51a9919c56 Answer A-3: the finding writer is generic over the contract's plain structs, replacing A-1's accessors
mesh/delivery closed: a newer head of the same pull request
mesh/check checking 51a9919c5622 on 60e99f38759b
Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 21:35:22 +01:00
jschoubben 3e7befcb0b Answer A-2: busarchive moves with bussnap into mesh-sdk
mesh/delivery closed: a newer head of the same pull request
mesh/check pass 3e7befcb0bb1 on 60e99f38759b | unit 43 run, 0 kept, 1 skipped | integration 43 run, 0 kept, 1 skipped | composition passed in 58s on 4…
Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 21:31:14 +01:00
jschoubben e95b7f8ea0 Answer develop's questions against a short list of principles, so a detail needs no design batch
The operator, 2026-11-11 about 21:30 CET: a living list of principles; the lead answers each question against it
and does not register every detail. Adds 00-META/principles.md, the lead-answers file G-20 with A-1, and lets the
developer take a lead answer from it.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 21:30:52 +01:00
jschoubben 7f094b8278 Write the outside-cause wait whole in G-19, so develop builds one mechanism
Round 1 of the review found §705 patched per finding: the commonest cause
(a machine down by its own network or silence) was missed, the keeper's
silence, the race between a check and its condition, the wait's ends, its
event fields, its finding's telling and its tools were unwritten. §706 now
says them once, following ADR 0677; §705 points to it. §707 gives the
writer's own finding-refused to each bundle's key part, with the statement's
bundle, in place of the G-15 edit, since G-15 is at its size cap. The
glossary's waiting call moves below the kept call's Not: line.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 21:22:52 +01:00
jschoubben 26f4b1c2a5 Hear O-9's outside cause from the condition keeper, and follow ADR 0676 in the gate
§705 said the assigner hears rewritten and unreachable from the node-engine's
finding events, but the node-engine only states the network; the watchman
raises those findings and the keeper holds them as conditions, and an
unreachable is raised on the machine that cannot be reached. The assigner now
reads the machine's conditions through mesh-conditions' verb at the judging
and judges again on condition-cleared, never reading the watchman's state.

§677 cited ADR 0259 §3, never in force; it now cites ADR 0286 §6 and ADR 0259
§6 with ADR 0676's clauses, says waiting call for the held call, and checks
what ADR 0676 decides. The glossary gains the waiting call and the holder that
asks for a marked verb served from a module's process. §580 gives the writer's
own finding-refused to every bundle within its own key part.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 21:13:37 +01:00
mesh-tools 60e99f3875 Merge pull request 'Design 47 milestone G: develop answers batches 6 and 7 (G-16 to G-18, §587-§705), the operator's answers O-1 to O-9 of 2026-11-11' (#552) from design/47-G-develop-answers-6 into main 2026-11-11 20:09:06 +00:00
jschoubben e6c8bb15d1 Hold a delivery on a machine whose network names an outside cause (O-9), and name mesh-host's wireguard places in §428
mesh/delivery merged: source.main moved to novox/hq@60e99f38759b
mesh/check being carried to e6c8bb15d1a7 on 60e99f38759b | from 108a56fbc78a
Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 21:04:00 +01:00
jschoubben 414490fa5a Close batches 6 and 7 by review round 3, ADR 0675 and ADR 0676, and develop's D-33 to D-49
Every must of the last review round is written as its reviewer proposed or
as the narrowest line: the gated call keyed by its own call id with one ask
per act across instances, the holder granted from its want, the keeper's
protected restore on a standing grant with a cancel and its partial stream
removed, the node-engine's kept entries in its composition state, the
indexer ordered before the parts that ask its entries, and G's own deletions
moving the gate's copy mark. Develop's D-33 to D-49 are §698 to §704.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 20:56:26 +01:00
jschoubben ccc8a4c492 Give every acting verb of mesh-recovery one answer form, cancel included (D-32)
Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 20:44:49 +01:00
jschoubben 350d05b2c2 Make a module's call of a marked verb one gated call (O-8), and answer develop's D-26 to D-31
The operator's O-8: the holder keeps a module's call, asks, and runs that
same call on the Approve; the caller hears the end on its own subject of
the seat's gated-ended and never calls again, while a person through the
tool runner keeps ADR 0286 §37's path for now (§677, resting on a record
to come). Develop's D-26 to D-31 and the reader release's order are §691
to §697, with §668, §682 and §554 corrected to them.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 20:44:03 +01:00
jschoubben 51c6dcbe46 Fix design 47 G's batches 6 and 7 by review round 2, O-7 and develop's D-19 to D-25
The holder of a marked verb a process serves now asks, checks and claims
the warrant itself, under its own name and in its own state, and a holder
of before G is never reached (§677). The keeper's snapshot is sent by the
keeper's own verb and a protected stream's never leaves it, restored in
place (§681, the operator's O-7). The recovery module's hand verbs get
their fields, answers and asker (§682); the node-engine keeps its entries
on its node (§683); the grants part composes a want's account before its
first views, so no cycle stands (§684). The republish queue is kept on the
bus, §588 rests on ADR 0674, and D-20 to D-25 are §685 to §690.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 20:35:34 +01:00
jschoubben 9381cde1b4 Fix design 47 G's batches 6 and 7 by the review round, and drop every named caller list
The four reviewers' musts on §587 to §676 are written into the lines they
name: the snapshot sent on its call's own session with its digest
recorded, warrants matched to the current ask and its digest, the gate's
copy at every start, the recovery module's hand tools as seat verbs, the
catalog half's pending entries and the node-engine following no branch,
the keeper following a moved finding stream, the builder's republish queue
and its toolchain wait, and more. The operator's answer of about 16:10 CET
ends every named caller list: every verb takes its class's default, and a
module's process that calls a marked verb carries the operator's warrant
for that one call (§677). Develop's D-16 to D-18 are §678 to §680, in a new
part G-18 so that G-17 stays under 60 KB.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 20:16:47 +01:00
jschoubben d004b90e49 Answer milestone G's seventh develop batch and the operator's answers of 15:14 to 15:42 CET
Batch 7 of design 47 G is written as §610 to §676 in G-17 (§614 was
committed ahead): the operator's answers on callers by class, on the five
parts that move only their findings, on the forge and image registry's
other entries, and on the gate's kept data; the refreshed build maps'
questions of mesh-tools, mesh-host and mesh-networking, mesh-sdk,
mesh-delivery, mesh-catalog and mesh-contracts; and develop's D-1 to D-15.
§588 now carries the operator's yes to publishing again on an event, its
record to come. Lines the answers correct are corrected in place, in G and
in design 34 every-event and design 46 7-approval-mark, which ship in G's
release. The main file now lists ADR 0668 and ADR 0672.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 15:54:59 +01:00
jschoubben 1012514fdc Say once how every part reads the catalog half per watched branch, so develop builds one form
Design 47 G §614 answers develop's D-5 and D-12 ahead of the rest of
batch 7: the reader through meshcatalogclient on the part's one views
reader, the branches from the registrations or the gate's source, the
manifest entries each part declares, an older build's entry asked once by
fingerprint, a none-yet branch read as empty, the first pass after every
first view, and a dropped branch's findings cleared.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 15:53:52 +01:00
jschoubben 0cdd66f945 Answer milestone G's sixth develop batch in design 47 so develop builds without choosing
Eight questions met in the join and develop after batch 5 are written as
lines §587 to §594 of a new part G-16, each of internal form within the
records and lines it cites: the ledger's upkeep calls' shapes, publishing
again every unpublished reserved version, a whole read of a module's build
keys, one event of a node's capabilities (manifest-reads-changed, ADR 0618
§4, ADR 0673 §2), the object stores' caps, the gate's merge path from G's
delivery, the gate's dropped Approve-per-merge reads, and the manifest
readers on the catalog graph entry. §568 now cites ADR 0673 in place of
"number to come". The automatic publish on a registry holder's running is
left to the operator: no record decides it.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 15:23:04 +01:00
mesh-tools 108a56fbc7 Merge pull request 'Design 47 milestone G: develop answers batch 5 (G-15 §560-§586), the operator's answers of 2026-11-11 afternoon' (#551) from design/47-G-develop-answers-5 into main 2026-11-11 13:37:27 +00:00
jschoubben 785cd229b3 Fix batch 5 by its review, so no cycle, lost act or lost pin is left to develop
mesh/delivery merged: source.main moved to novox/hq@108a56fbc78a
mesh/check checking 785cd229b310 on 108a56fbc78a
Writes round 1's musts into the lines they name. A build edge is met by a running
holder. An act cut off by a move ends through its failure path. Pins survive a
race and a restart, and the gate restates them. Each point's digest is kept on the
bus. The words of a failed build move to the assigner's hold. Also writes the
operator's answers of 14:55 and 15:15 CET: three automatic rebuilds and then an
ask; the image registry copies its template keys at its first start; refused asks
kept are a setting.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 14:35:30 +01:00
jschoubben e5d199263b Make the forge module's readings of step-ended design lines, and home the reserved client in its seat's contract
Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 14:31:46 +01:00
jschoubben 8999f8e9d0 Answer design 47 G's fifth develop batch, so develop can go on without choosing
Writes §560 to §584 (G-15) and corrects the lines of G-1, G-12 and G-14 those
answers name. They cover the recovery module's events, findings, cap, hand tools
and digest check; the dump giving way to a restore; the builder's reads and its
retry on an event; mixed build and delivery cycles; refused asks; mesh packages
in lock files; and the forms the developers asked about. They carry the
operator's answers of 2026-11-11 (14:05, 14:15 and 14:30 CET): a put-off dump is
that night's backup; the recovery module moves freely; it is one process holding
its seat; refused asks are listed; a failed build says so; and the forge and the
image registry modules join the foundation.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 14:31:46 +01:00
mesh-tools 6280fc3a3e Merge pull request 'Agents: hq-record-writer, the definition of the agent that proposes a record from a draft' (#550) from agents/hq-record-writer into main 2026-11-11 13:18:34 +00:00
jschoubben b4abb98aaa Give the record writer a definition, as a role started more than once
mesh/delivery merged: source.main moved to novox/hq@6280fc3a3e2e
mesh/check checking b4abb98aaabc on 6280fc3a3e2e
The lead started an agent that proposes a record from a draft by hand in two sessions; agents-have-a-home asks a definition.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 14:17:07 +01:00
mesh-tools 1c6fe9611c Merge pull request 'Design 25 bindings-first-view §9: compose a bucket's binding from the bus module's list answer, breaking the registry circle (issue 1966)' (#549) from design/25-bindings-from-the-list into main 2026-11-11 12:50:07 +00:00
jschoubben f65acb98c7 Compose a bucket's binding from the bus module's existing list answer, breaking the registry circle
mesh/delivery merged: source.main moved to novox/hq@1c6fe9611ccc
mesh/check checking f65acb98c742 on 1c6fe9611ccc
The bus module's new build cannot be built: its image needs the registry's
mirror, which the registry holder serves only with a state binding the grants
part never heard and waited for that very build to say. The operator chose
(2026-11-11 13:30 CET) to compose key-value and object-store bindings from the
fields the running bus module already answers, delivered with the grants part
alone. Records that §7's 'needs no edge' missed the bus module's own build
through the registry.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 13:48:10 +01:00
mesh-tools 51b43dfb10 Merge pull request 'Design 47 milestone G: develop answers batch 4 (G-12, G-14), the recovery module and the operator's answers of 2026-11-11' (#548) from design/47-G-develop-answers-4 into main 2026-11-11 12:14:42 +00:00
jschoubben 040ad22e69 Keep setting aside and discarding an aside on the operator's hand tools, refused by restore-kept
mesh/delivery merged: source.main moved to novox/hq@51b43dfb10d1
mesh/check checking 040ad22e691d on 51b43dfb10d1
Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 13:12:42 +01:00
jschoubben e164a66362 Write the operator's 13:10 answer: a restore asked is done, identity being the one check
mesh/delivery closed: a newer head of the same pull request
mesh/check checking e164a6636261 on cbafc7556a40
Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 13:08:53 +01:00
jschoubben caa7a9265c Make identity the one check of the recovery module's callers, and give a bundle's finding writer its two runtime calls
Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 13:08:11 +01:00
jschoubben 0b90251098 Write the operator's answers of 13:06 on setting aside and on discard-aside's check
Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 13:06:37 +01:00
jschoubben a3d9975e4b Answer the last review round of batch 4: the recovery module's acts, order, callers and grants
Its verbs answer on acceptance and its events carry the act's form and fingerprint,
it never restores beside the nightly dump either way, a node-engine may call it only
for a want its node holds, and its grants are listed whole as ADR 0672 has them; the
long form of an invokes entry is read by the second reader release first.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 13:06:14 +01:00
mesh-tools cbafc7556a Merge pull request 'Design 47 milestone G: develop answers batch 3 (G-10, G-11, G-13), with the operator's answers of 2026-11-11' (#547) from design/47-G-develop-answers-3 into main 2026-11-11 11:57:47 +00:00
jschoubben 6d0506402d Put restoring kept data on a recovery module, and answer round 1 of the review of batch 4
The operator chose a module of its own for snapshotting and restoring a module's
kept data on the live bus, so the bus module and the snapshot account keep their
narrow grants; the reviewers' musts are written into the lines they name, and the
new lines go to G-14 so G-12 stays within a part's size.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 12:57:01 +01:00
jschoubben e504dac534 Write the operator's answers of 12:41 on restore points, the checker's drop and an empty step status
Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 12:42:09 +01:00
jschoubben e41dbeef4a Write the operator's answer on an Approve before G, and answer round 3's leftovers of the third batch
The operator made an old Approve that pull request's approval step, decided by the
registration's policy, so §510's carried act and §608's merge wait go; round 3 of
the third batch has no fourth round, so its findings are answered here (§543 to §550).

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 12:40:58 +01:00
jschoubben 05b16d63bb Answer develop's fourth batch of questions on milestone G, and write the operator's words for the teller
Develop met questions the design did not answer on restore points, a bundle's
channel to the tool runner, the want's new fields, the finding stream's form
under ADR 0595 and ADR 0666, and per-branch views; each is now a design line
(G-12, §516 to §541) with its reason and check, so develop builds them and
decides nothing.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 12:38:56 +01:00
jschoubben 529cba366b Answer the last review round of milestone G's third batch, in G-13 from §600
mesh/delivery merged: source.main moved to novox/hq@cbafc7556a40
mesh/check being carried to 529cba366b07 on cbafc7556a40 | from ec6be19b1138
The four reviewers' round-2 findings are answered as design lines: the build seats' accept subjects, workers and
grants and the rollout's order as one seam, an ask's start and end outside a pipeline, the builder's heard
toolchains, the tool runner's order from the package a build resolved, the step log's single write, the objects
stores' caps and a carried merge's wait.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 12:29:39 +01:00
jschoubben 298e68c71f Write the operator's noon answers, the review of §456 to §479, and develop's next answers into milestone G
The operator answered who may drop a build seat's steps, whose name a step key carries, and that a review in G
refuses no one for authorship. The four reviewers' musts on G-10 are answered as design lines, and develop's next
questions of internal form (the watchman's, builder's, indexer's, forge bridge's, assigner's, view's, bus module's,
keeper's and node-engine's) in the new part file G-11 (§480 to §515); the teller's words wait for the operator.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 12:08:39 +01:00
jschoubben e9ae4afc12 Answer develop's third batch of questions on milestone G, and let the design writer answer internal forms
The operator answered D-12, D-13 and D-19 at the terminal, and ruled on 2026-11-11 that a question touching nothing
the operator sees, no permission and no data kept is answered as a design line by the design writer and read by the
reviewers by architectural domain. The new part file G-10 carries those answers and the internal-form ones (§456 to
§479), the earlier lines point to them, and playbooks 02, 07 and 08 and the glossary say who answers a design
question now. The questions that touch who may call or what the operator sees are left for the operator.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 11:52:51 +01:00
mesh-tools ec6be19b11 Merge pull request 'A developer reads its build map and the design lines it cites, not the whole design' (#546) from process/developer-reads-its-lines into main 2026-11-11 10:32:29 +00:00
jschoubben ae1b5ee957 A developer reads its build map and the design lines it cites, not the whole design
mesh/delivery merged: source.main moved to novox/hq@ec6be19b1138
mesh/check checking ae1b5ee957ad on ec6be19b1138
Four developers of design 47's milestone G ran out of context on 2026-11-11 after one or two subtasks: the
milestone part and the main file they were told to read whole come to about 580 KB.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 11:31:14 +01:00
mesh-tools 9ca4757357 Merge pull request 'Design 47 milestone G: develop answers D-7 to D-10, and design 25's types wording' (#545) from design/47-G-develop-answers-2 into main 2026-11-11 09:56:58 +00:00
jschoubben 073160ca46 Answer develop's next questions on G's contracts, and correct design 25 on the bus module's types
mesh/delivery merged: source.main moved to novox/hq@9ca4757357d2
mesh/check checking 073160ca46f1 on 9ca4757357d2
The generator emits body types and sending stays mesh-sdk's, the reader release reads needs-approval and the
bodies map, an accepted kind's name is separate from its stream, and the bus module's created and recreated do
carry types.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 10:55:20 +01:00
mesh-tools a1cbcfb23f Merge pull request 'Design 47 milestone G: answers to develop's first questions (callers of the bus module's acts, the node-engine's verbs, small body forms)' (#544) from design/47-G-develop-answers into main 2026-11-11 09:46:41 +00:00
jschoubben 4976caa124 Answer develop's six questions on milestone G's contracts, so the seat definitions are written from the design
mesh/delivery merged: source.main moved to novox/hq@a1cbcfb23f69
mesh/check checking 4976caa12443 on a1cbcfb23f69
Develop met verbs and events in mesh-contracts whose callers, classes, states or bodies no line named: the bus
module's acting verbs, the node-engine's thirteen verbs, the keeper's findings-views, the grants part's fill
events, two answers and three field forms.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 10:45:18 +01:00
mesh-tools 6c1e759e26 Merge pull request 'Design 25: the grants part takes a first view of the bus module's bindings at every start, so a missed created event is recovered (issue 1966)' (#543) from design/25-bindings-first-view into main 2026-11-11 09:27:00 +00:00
jschoubben d0760629e5 Shape the seat's Binding as the bus module sends it
mesh/delivery merged: source.main moved to novox/hq@6c1e759e267b
mesh/check being carried to d0760629e5fe on 6c1e759e267b | from f1fe609b3e85
The seat definition's Binding named a bus name and subjects while
created and recreated carry mesh-sdk's membership binding, which the
grants part needs; one form for the events and list's answer means the
generated contract must decode what is on the wire, unchanged.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 10:14:07 +01:00
jschoubben 27ec6f8500 Make the grants part one of G's observers, so its waits are findings
mesh/delivery closed: a newer head of the same pull request
mesh/check pass 27ec6f8500da on f1fe609b3e85 | unit 43 run, 0 kept, 1 skipped | integration 43 run, 0 kept, 1 skipped | composition passed in 91s on 3…
G-4 §200 already had the grants part write view-waits as a finding, while
G-8 §387 left it out of the observers that say findings as events. Adding
it, with its state findings and finding stream in its old sections, lets
the bindings first view's two conditions move to findings at G's delivery.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 10:08:04 +01:00
jschoubben c098518cc8 Design 25: the grants part asks the bus module for every binding at its start
A binding was written only on the bus module's live created event, so one
event the grants part never heard lost the binding for good (issue 1966):
a registry holder lacked three entries the bus held, and its publishes
were never said published. The operator chose to ask the bus module: at
every start and on a re-ask the grants part takes a first view from the
seat mesh-broker's list, whose entries now carry their binding, and
composes the bindings from it; a binding the bus no longer lists is kept
and said, never removed by a first view.

The same change puts design 25 in playbook 02's shape: section 12.2 to
12.6 move, unchanged and numbered, into the part of milestone bus-state,
so the main file falls from 104 KB to about 71 KB.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 10:08:04 +01:00
mesh-tools f1fe609b3e Merge pull request 'The approval mark and every-event: the prepare's answers, so both parts are complete for develop with milestone G' (#542) from design/34-wait-on-any-event into main 2026-11-11 08:56:23 +00:00
jschoubben 8e9116469a Answer step 0b's review of the approval mark and marked waits
mesh/delivery merged: source.main moved to novox/hq@f1fe609b3e85
mesh/check being carried to 8e9116469a0a on f1fe609b3e85 | from fbd7981b7be2
The served version comes from what the bundle runs and a reading from the
runner that made it, so no placement or clock lifts a deny; the runner's
state takes the old form the bus module can create; kept calls stay open on
their key; and the closed directory's mode is read by node-files.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 03:26:27 +01:00
jschoubben 28c22c65cb Answer the prepare's questions on the approval mark and marked waits
Each of the 25 recommendations becomes a design line with its question id,
so the builders find every field, test and source named; the live readings
of the credentials go into the gate's stated bound.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 03:24:13 +01:00
mesh-tools fbd7981b7b Merge pull request 'Design 47 milestone G: the prepare's answers, so the design is complete for develop' (#541) from design/47-G-prepare-answers into main 2026-11-11 02:17:01 +00:00
jschoubben b4a07c1de6 Answer the step-0b review of G-9: the catalog entry carries its manifest, grants follow the placed build
mesh/delivery merged: source.main moved to novox/hq@fbd7981b7be2
mesh/check checking b4a07c1de6f6 on fbd7981b7be2
The review found that reading the catalog graph instead of manifests needed the entry to carry the manifest and
stay while a build names it, the grants to follow the wanted build, the pre-G node-engines their manifests by
digest, and the restore, caller, stream, finding and test-budget lines a name for every field they left open.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 03:15:45 +01:00
jschoubben 30b29a58a4 Have only the indexer read manifests, and settle the four prepare items that contradicted each other
The operator's rule is that the indexer alone uses manifests to build the catalog graph, so every other part's
read of a manifest becomes a read of the catalog half. Lead-47's rulings settle the finding layout as a seat
contract with generated writers, process-builds as a node-engine seat verb, a grants-for read on mesh-grants for
widens, and aside on node-backup.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 03:15:45 +01:00
jschoubben b08147e531 Answer the questions of milestone G's final prepare in its part, so develop starts from the design alone
The prepare found about a hundred places where G's lines left the builder a choice. Each answer is now a line of
G-9 with its question and its check: one default of callers per verb class, so no verb is closed for lack of a
list; the seat-definition forms; kept data and restore points; versions; steps and test counting; the readers and
the gate's remaining parts. Earlier lines the answers change are amended in place.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 03:15:45 +01:00
mesh-tools 7dfb6f0b7b Merge pull request 'Designs 34 and 46: sessions wait on any event, and the approval mark gates calls and marked waits, delivered with design 47's milestone G' (#540) from design/34-wait-on-any-event into main 2026-11-11 02:08:01 +00:00
jschoubben 95ae1dd6ed Read the secrets list and every mark from the module catalog alone
mesh/delivery merged: source.main moved to novox/hq@7dfb6f0b7b15
mesh/check being carried to 95ae1dd6ed90 on 7dfb6f0b7b15 | from c74546a16f78
The operator answered that only the indexer uses the manifests, to build
the module catalog; so the secrets review and the tool runner read the
catalog revision, and the bus read and its nats tool extension go.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 03:03:00 +01:00
jschoubben de62fe5852 Answer round 3 of the domain review of the approval mark
mesh/delivery closed: a newer head of the same pull request
mesh/check pass de62fe58523a on c74546a16f78 | unit 43 run, 0 kept, 1 skipped | integration 43 run, 0 kept, 1 skipped | composition passed in 69s on 4…
The client's node prefix stays on its own keys, so the router's record is
still read where the router writes it; the remaining notes say where the
runner's version is read and what each limit costs.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 02:53:20 +01:00
jschoubben bddf6ce95a Answer round 2 of the domain review of the approval mark
A stuck state recovers as a kept call, a node with no or stale readings is
denied, the caller-named subjects are withheld too, and the per-machine key
form and the client's prefix are named, so no path serves a marked tool
unasked; the forge's loosening is marked by the operator's answer.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 02:51:24 +01:00
jschoubben 636b2be7ac Write the operator's answers on the approval mark into milestone 7
Call again stands by the operator's choice; the credential file's reach is
a known limit, as G's shared account is; and the forge's two loosening acts
move to a marked tool of their own, so tightening stays unasked.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 02:48:10 +01:00
jschoubben a6c9b96be3 Answer round 1 of the domain review of the approval mark and marked waits
The bus withholds a marked subject from a runner that does not honour the
mark again, as ADR 0286 has it; marks follow G's catalog half; a recording,
a stuck gate, an absent router and a start before the marks are read each
have a stated answer, so nothing runs unasked and nothing fails silently.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 02:46:01 +01:00
jschoubben ca1fbf7867 Build the approval mark in milestone G and let a marked wait use it
The operator answered "Build the gate in G" and wants no new records: the
marked-wait lines now hold by the operator's answers, each departure said in
its Why, and design 46's milestone 7 becomes a part a builder reads whole,
written to the mesh as built (marks read from the bus, the capability hold,
the client's claim in bus state, the hand-act seat), delivered with G and
every-event.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 02:41:11 +01:00
jschoubben aec610a6f4 Design 34 every-event: hear every module stream, and gate a marked wait on the operator's warrant
The operator chose every module stream, mail too (ADR 0671), so the stream
lines hold and the restricted-stream deny goes. The operator also asked that
a wait respect the approval mark; that extends ADR 0286 to events and has the
tool runner's gate hold it, so those lines are written pending the record.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 02:41:11 +01:00
jschoubben d758964ca9 Design 34 every-event: the review reads a stream's subjects with nats_streams
No tool answered which subjects a stream holds, so the secret review could not find an
event the list missed; a module's tools read everything it holds, so the nats module's
tool gains that read, built first as a small fix.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 02:41:11 +01:00
jschoubben c232b1fb81 Design 34 every-event: write in the four domain reviews' findings
The reviews found waits that time out for ever on a misspelt name, refusals that would
loop a watcher, an acceptance no answer could prove, readers still describing the list,
and no way back said. Each is now a design line; the stream-events lines are written for
the recommended option and marked pending the operator's answer.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 02:41:11 +01:00
jschoubben 024627f658 Design 34: a session waits on any event, the tool runner hearing every one (ADR 0670)
Watchers reread on a timer because mesh_wait refuses every event outside a list of five,
and the delivery status view's events, which playbooks have watchers wait on, are not on
it. ADR 0670 grants the tool runner every event subject and drops the list; this writes
that as milestone every-event, one delivery group of mesh-sdk, mesh-delivery and
mesh-tools, and puts design 34 in the main-file-and-parts shape.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 02:41:11 +01:00
mesh-tools c74546a16f Merge pull request 'Design 47: milestone G's part complete, every operator answer and three review rounds written in' (#539) from feat/47-G into main 2026-11-11 01:34:16 +00:00
jschoubben 87827ebb82 Answer round 3 of the review of milestone G: a home and trust for the finding events, restarts that keep standing faults
mesh/delivery merged: source.main moved to novox/hq@c74546a16f78
mesh/check checking 87827ebb82a2 on c74546a16f78
The finding events lacked a stream, a consumer per source, a rule against a restart clearing what still stands, a
take-over rule and presence events; the 2026-11-11 answers move to a part file of their own so each stays under its
size. The test budget is counted per module, the upgrade window between the builder and the registry holders gets
its edge, and the readers of the new manifest words come first.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 02:30:59 +01:00
jschoubben bfd5fd2c0b Hear an event every declaring module says, keep the connectivity verbs open, and clear findings of a node a module left
The keeper finds its sources in the catalog and names none in its manifest, so one generic consumes form with its
validator rule and per-module grant carries the finding events; the operator's answer that a verb open today stays
open covers the connectivity verbs that name no callers; and findings of a module no longer placed on a node would
otherwise stand forever.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 02:21:43 +01:00
jschoubben 9f9cb58f54 Write the operator's answers into milestone G's part: findings heard as events, invokers chosen, the shared account a known limit
The condition keeper read other modules' findings states, which only their owner may reach; a node-engine
restarted with the full seat list never cleared its own finding, because only its process remembered writing it.
Observers now say each finding as an event and state all they own at each start and on the keeper's ask, so the
keeper's view is rebuilt from events alone. The `invokers` form is the operator's choice, and the tool runner's
shared account stays as it is in G, named as a known limit.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 02:20:24 +01:00
jschoubben 9bcd0ad197 Drop the lint scan for fixed names and keep "no part fixes a name" as a guideline the code review reads
The operator, 2026-11-11: the scan was a flaky check, and the rule is a guideline, not a decision. The names
G's own work removes are removed by its subtasks; the exemption line and the scan's checks go with it.

Claude-Session: https://claude.ai/code/session_01JG73KK8HD1gufnqyFxMFdW
2026-11-11 02:06:46 +01:00
jschoubben 27c9c6f11f Answer round 2 of the review of milestone G's part: one rebuild trigger, digests from events, every new store sized
The four reviewers found two triggers for one rebuild, digests read from the builder's own keys, a mesh-wide wait
with no owner, bound or tool, new states with no retention or cap, a first fill the builder could race, and a
registry check whose source goes; each is now a design line with its reason, and test 60 follows section 271.

Claude-Session: https://claude.ai/code/session_01JG73KK8HD1gufnqyFxMFdW
2026-11-11 02:06:46 +01:00
jschoubben 893d83d23b Rebuild only on the edges a part is made from, wait for a delivery mesh-wide, and name the version in each want
The operator answered on 2026-11-10 that only an imported package and the build toolchain make a dependent stale,
every other edge only ordering, that a dependent waits for its dependency's delivery to every present machine,
that a want names the released version and each part's digest, and that one tool answers a part's edges both
ways. G's answers move into a file of their own so each file of the part stays under 60 KB.

Claude-Session: https://claude.ai/code/session_01JG73KK8HD1gufnqyFxMFdW
2026-11-11 02:06:46 +01:00
jschoubben 5df2eca59e Compare a release's edges by the dependency part's digest and per node, and fill them once at the start
A module's version can change without changing the part an edge points to, a delivery happens per node, a build
of another branch has no version, and acting and serving are facts, not versions: the lead's review of §374 to
§376 found each would mark edges behind wrongly or redo work. A first fill keeps the first query from finding
the whole mesh behind.

Claude-Session: https://claude.ai/code/session_01JG73KK8HD1gufnqyFxMFdW
2026-11-11 02:06:46 +01:00
jschoubben b157283202 Record on each released version what it was built against, and find what is behind with one query
The operator chose on 2026-11-10 that the catalog of versions keeps a lockfile per released version (issue
1961): each edge's dependency version, so one query answers what is outdated after a restart or a missed event,
while each edge kind is still started by its own event, a release or a delivery.

Claude-Session: https://claude.ai/code/session_01JG73KK8HD1gufnqyFxMFdW
2026-11-11 02:06:46 +01:00
jschoubben f215cb9207 End tagged versions at milestone G, since no custom versions exist for now
The operator's answer of 2026-11-10 allows no custom version, and a tagged version is one: a group's run on
request delivers nothing, so it builds its members against each package member's build by digest and publishes
no version, and `tag-wanted` and `tag-asks` go with G's delivery.

Claude-Session: https://claude.ai/code/session_01JG73KK8HD1gufnqyFxMFdW
2026-11-11 02:06:46 +01:00
jschoubben c46669a17e Have design 51 reserve a module's version at its build's start, as design 47's milestone G does
Design 51 still numbered a package at its publication and said `version.numbered`, which the operator's choice
of 2026-11-10 replaces: the catalog's released versions are the one list, and a packed file carries its version.
The catalog's first versions are filled from what is published, so numbering continues above today's.

Claude-Session: https://claude.ai/code/session_01JG73KK8HD1gufnqyFxMFdW
2026-11-11 02:06:46 +01:00
jschoubben e27fd02269 Number a module's version before its build, as the operator chose, so packed files carry it
A Go module zip and a TypeScript package hold their version inside the file, so a number given at publication
could not be valid for a digest built before it (gaps items 22, 23, 43). The operator chose on 2026-11-10: the
manifest says MAJOR.MINOR, the build system reserves the next patch at the build's start, the catalog holds and
says released versions only, the first watched branch alone releases, and Go packages are tagged, departing from
ADR 0573 §2 until a later research brings the record in line.

Claude-Session: https://claude.ai/code/session_01JG73KK8HD1gufnqyFxMFdW
2026-11-11 02:06:46 +01:00
jschoubben 63eeb96ebd Answer round 1 of the review of milestone G's part, and split the part so each file is read whole
The four reviewers by architectural domain found retentions judged from another part's records, unbounded aging,
waits and stores no condition names, a kept artifact checked against the builder's key, and caller limits left
open; each is now a design line with its reason. The part is six files under 60 KB, one milestone still, with
G.md as its index; the main file stays over its bound on the lead's word.

Claude-Session: https://claude.ai/code/session_01JG73KK8HD1gufnqyFxMFdW
2026-11-11 02:06:46 +01:00
jschoubben da83f29f47 Give milestone G its own part of design 47, and answer its builders' open questions there
G's builders read a 443 KB design by searching it and filled what they did not find; playbook 02's shape gives
each milestone a part read whole. G's lines move whole into 47-milestones/G.md, numbered as design lines, and
the questions G's prepare and develop met within the records are written as the design's own choices
(the operator, 2026-11-10: small design choices are the design's, only architectural ones are asked).
The size bounds are bent for this split on the lead's word; research 087 takes them up.

Claude-Session: https://claude.ai/code/session_01JG73KK8HD1gufnqyFxMFdW
2026-11-11 02:06:46 +01:00
mesh-tools 000a35d5cf Merge pull request 'Design 47: the watchman's take-in seed ends waits the indexer no longer holds (issue 1966)' (#538) from design/47-seed-ends-stale-waits into main 2026-11-10 22:45:34 +00:00
jschoubben b58d789e83 Design 47: the watchman's take-in seed ends waits the indexer no longer holds
mesh/delivery merged: source.main moved to novox/hq@000a35d5cfce
mesh/check checking b58d789e83d0 on 000a35d5cfce
The operator's answer to the design question of issue 1966 note 195: a take-in
wait whose taken event was never heard stood for ever; the indexer's pending
state is what waits. Also says the indexer's taken event covers every key its
pending tool named.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-10 23:44:03 +01:00
mesh-tools e65d0f8f2b Merge pull request 'Build features from the design alone and ask the operator what it leaves open' (#537) from process/implement-the-design into main 2026-11-10 16:51:25 +00:00
jschoubben cdfed7593e Keep the check step's no-second-review sentence whole
mesh/delivery merged: source.main moved to novox/hq@e65d0f8f2bbd
mesh/check being carried to cdfed7593e73 on e65d0f8f2bbd | from b229f5c4df1e
Claude-Session: https://claude.ai/code/session_01JG73KK8HD1gufnqyFxMFdW
2026-11-10 17:24:12 +01:00
jschoubben 6e90fa443e Compare seat contracts with the design in the prepare, and call a verdictless merge unchecked
On 2026-11-10 a seat contract and an event's subject that differed from design 47
surfaced only in develop, and the checker twice dropped a check still running when
its pull request merged. The prepare now compares every seat contract the milestone
touches with its design lines, each difference a design question before develop;
and a merged change with no verdict is reported as unchecked, never as passed.

Claude-Session: https://claude.ai/code/session_01JG73KK8HD1gufnqyFxMFdW
2026-11-10 17:24:02 +01:00
jschoubben 8f9a6d4f7b Build features from the design alone and ask the operator what it leaves open
mesh/delivery closed: a newer head of the same pull request
mesh/check checking 8f9a6d4f7bc9 on b229f5c4df1e
Milestone 47/G was prepared and developed from a 430 KB design by agents told to
start from the knowledge base and judge what was not mesh-shaped. The prepare wrote
its own findings and about 40 rulings by two leads, seven of which went against the
records or the design, and 29 questions the design never answered surfaced only in
develop.

The playbooks now make the milestone's part of its design the only input to the
prepare, develop and the code review: the prepare outputs a build map per
repository and the design questions; the operator answers each and the answer is
written into the design before develop starts; every commit cites the design lines
it builds and the review judges it against them. A design is a main file and one
part per milestone, within a size an agent reads whole. Also: one lead per
milestone, a contract's users start from its commit, one writer per file, one
sibling placement, and agents stop between subtasks, never inside one.

Claude-Session: https://claude.ai/code/session_01JG73KK8HD1gufnqyFxMFdW
2026-11-10 17:21:06 +01:00
mesh-tools b229f5c4df Merge pull request 'Take back three design 47 choices that broke accepted decisions (issue 1966)' (#536) from design/47-G-undo into main 2026-11-10 15:52:22 +00:00
jschoubben ebf115303a Undo three of design 47's milestone G choices that went against accepted decisions
mesh/delivery merged: source.main moved to novox/hq@b229f5c4df1e
mesh/check checking ebf115303a87 on b229f5c4df1e
The joint amendment wrote three choices that the records already decide
otherwise. The foundation parts are again the manifest validator's list, the
runner added (ADR 0666 §4), with no registration value and no finding of their
own. The runner again starts a group's pipeline on the indexer's take-in and
consumes package-published (ADR 0666 §13, §15); a second start for the same
revision and module writes nothing by the pipeline key's compare-and-set. A
replaced object stays, like a key-gone one, until the drop tool on a person's
word (ADR 0595 §4). Design 51 and the glossary follow.

Claude-Session: https://claude.ai/code/session_01JG73KK8HD1gufnqyFxMFdW
2026-11-10 16:50:38 +01:00
mesh-tools 3d97fe016c Merge pull request 'Amend design 47 with milestone 47/G's joint findings as its own choices (issue 1966)' (#535) from design/47-G-joint into main 2026-11-10 15:33:22 +00:00
jschoubben b33ac325c0 Amend design 47 with milestone 47/G's joint findings as its own choices
mesh/delivery merged: source.main moved to novox/hq@3d97fe016c2d
mesh/check being carried to b33ac325c0ec on 3d97fe016c2d | from d0d71b554876
The prepare found where ADR 0666, ADR 0667 and design 47 disagreed. The
ADR 0666 insight was dropped, so its five points are written here as the
design's choices with their reasons: the foundation from the registrations
(departing from ADR 0619 §3 for ADR 0666 §5), the want's compare-and-set per
module and watched branch, every pipeline started on catalog-revision, the
cost of a refused new indexer, and each check at its row's level. ADR 0667's
clause numbers follow the record as accepted, so the merge token goes only at
§12's moment and on the operator's word. The approval is the operator-channel's
accepted kind, a replace removes its object, the sent store's age is
transport, and what the milestone creates has its retention. The glossary
gains "held back" and "sent store"; design 51 starts a publication's
pipelines on catalog-revision.

Claude-Session: https://claude.ai/code/session_01JG73KK8HD1gufnqyFxMFdW
2026-11-10 16:13:45 +01:00
mesh-tools d0d71b5548 Merge pull request 'Close a research effort with the knowledge base's close verb (issue 1966)' (#534) from fix/1966-close-verb into main 2026-11-10 09:58:42 +00:00
jschoubben c814bf2e0f Close a research effort with the close verb, so only the closing is judged
mesh/delivery merged: source.main moved to novox/hq@d0d71b554876
mesh/check checking c814bf2e0f18 on d0d71b554876
Proposing a graduated effort again whole had the knowledge base re-judge its approved reasoning
against the decisions that replace it (issue 1966).

Claude-Session: https://claude.ai/code/session_01JG73KK8HD1gufnqyFxMFdW
2026-11-10 10:57:23 +01:00
mesh-tools 2ba88971cf Merge pull request 'Glossary: transport fault and kept call; developers run the touched packages' integration tests once per head and use the live seat list (issue 1966, ADR 0668)' (#533) from agents/1966-lessons into main 2026-11-10 07:33:26 +00:00
mesh-tools 38915bbe01 Merge pull request 'Amend designs 47, 46 and 51 for pipelines per module and milestone G as one milestone (ADR 0666, ADR 0667)' (#532) from design/086-pipelines into main 2026-11-10 07:20:44 +00:00
jschoubben 78ef41e7bd Make playbooks 07 and 08 agree on the developer's integration run, and bound it
mesh/delivery merged: source.main moved to novox/hq@2ba88971cf52
mesh/check checking 78ef41e7bdb7 on 2ba88971cf52
The first amendment left the step table, the review's reading of integration
tests, step 3 and playbook 08 saying the opposite. The run is now once per head
reported, the touched packages and their importers in the module, by name where
the importers are many, and never a whole module or level, within the operator's
rule of 2026-10-18. The live seat list is read again when a head is reported.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-10 06:53:26 +01:00
jschoubben 7bbd4a86b4 Name transport faults and kept calls, and make developers check what the review found
Issue 1966's fix group showed two gaps in how a developer checks: an integration
test the change broke went unseen because only the untagged suite ran, and a stale
seat list made the local manifest validator refuse what the running one accepts.
The developer definition and playbook 07's develop step now run the touched
packages' integration tests against an embedded bus, the whole of mesh-sdk, and
the validator against the live seat list and a fresh catalog trunk, and stop at
two thirds of the context with a handover in the prepare file. The glossary gains
transport fault (ADR 0664 §1, ADR 0668 §1) and kept call (ADR 0668).

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-10 06:51:43 +01:00
jschoubben 43eac215d2 Follow ADR 0666's 26 clauses in the designs and the glossary
mesh/delivery merged: source.main moved to novox/hq@38915bbe0174
mesh/check checking 43eac215d264 on 2ba88971cf52
The record was proposed again with clauses 2 and 17 split for the
knowledge base's review, so every citation of it moves to the new numbers.

Claude-Session: https://claude.ai/code/session_01JG73KK8HD1gufnqyFxMFdW
2026-11-10 03:11:29 +01:00
jschoubben 40661df80f Take in round 3's last two fixes and record the three review rounds
mesh/delivery closed: a newer head of the same pull request
mesh/check pass 40661df80ff4 on 6611b94e3d7a | unit 43 run, 0 kept, 1 skipped | integration 43 run, 0 kept, 1 skipped | composition passed in 114s on …
Seams and tools and security and upgrade approved with two fixes: only an
operator's or a healer's rerun clears a failed step for merge-pull, and a
fix before the first registrations is merged by the operator under the
trunk-only want. The review section now records all three rounds.

Claude-Session: https://claude.ai/code/session_01JG73KK8HD1gufnqyFxMFdW
2026-11-10 03:02:39 +01:00
jschoubben 45cc27cf24 Take in round 2 of the review by architectural domain of the 086 amendment
Seams and tools and security and upgrade asked seven items, ruled by
fix-1965 with ADR 0666 and ADR 0667 staged to match: the assigner and
merge-pull switch on the first registrations, a branch that lost its
protection starts nothing, a passing rerun clears a failed step, sent
objects are read by a grant derived from the manifests, and the object
store is a small mesh-sdk package so nothing else rebuilds. The staged
record renumbered its clauses, so every citation of ADR 0666 follows the
new numbers.

Claude-Session: https://claude.ai/code/session_01JG73KK8HD1gufnqyFxMFdW
2026-11-10 03:00:45 +01:00
jschoubben a8103cf0b1 Take in round 1 of the review by architectural domain of the 086 amendment
The four reviewers asked changes in every domain and fix-1965 ruled on each,
with ADR 0666 and ADR 0667's wording staged to match: foundation parts keep
old sections, merge-pull judges from its own state so a head merges while
the runner is down, a group's wants wait for group-ended, a failed head is
said to its author then the operator, the checker goes only after a first
end-to-end pipeline and a tested backup, and the stores are sized again for
publish rebuilds.

Claude-Session: https://claude.ai/code/session_01JG73KK8HD1gufnqyFxMFdW
2026-11-10 02:49:51 +01:00
jschoubben bce14ad12e Hold no requirer of bus state for a bus build that waits for a yes
The assigner's provider rule read the phase of the bus module's newer want,
so a yes not yet asked held every module asking bus state (issue 1966,
note 150); milestone G reads what runs. Also name the tracker of the
review-author stopgap.

Claude-Session: https://claude.ai/code/session_01JG73KK8HD1gufnqyFxMFdW
2026-11-10 02:39:49 +01:00
jschoubben 55ac9f2d8e Name in design 47 the records its pipeline text now follows
Claude-Session: https://claude.ai/code/session_01JG73KK8HD1gufnqyFxMFdW
2026-11-10 02:38:40 +01:00
jschoubben 3de6895e32 Give the glossary research 086's words for pipelines
Research 086's Domains section names the words its graduation adds and
changes; the designs now use them, so the glossary defines them in the
same set: pipeline, pipeline step, policy, registration, watched branch,
run on request, artifact reference, the seats mesh-pipeline-runner and
mesh-builds, the composition step and the build-input fingerprint, with
Homonyms rows for policy and step. The words that leave (merge check, try,
trunk commit, merge turn, composition check) keep their entries until
milestone G's delivery, since they govern it, and move to Not: lines with
the as-is update after it.

Claude-Session: https://claude.ai/code/session_01JG73KK8HD1gufnqyFxMFdW
2026-11-10 02:38:13 +01:00
jschoubben 2fa15a1c8d Write ADR 0666's asks into design 46 and its versions into design 51
ADR 0666 §6 has the operator-channel offer a fourth ask instead of refusing
it, never holding the newest back; design 46 now says which ask waits and
how it is offered again. ADR 0666 §9 numbers a package when it is
published and has that publication rebuild its dependents; design 51 now
says how the builder numbers it, says it on the seat mesh-builds, and who
asks a tagged version once the checker goes.

Claude-Session: https://claude.ai/code/session_01JG73KK8HD1gufnqyFxMFdW
2026-11-10 02:36:26 +01:00
jschoubben 471fd8791d Write research 086's pipelines into design 47, one milestone with G
ADR 0666 and ADR 0667 decide that a change goes from a commit on a watched
branch to the machines through its module's pipeline, with no Approve per
merge and the checker gone, built and delivered as one milestone with
design 47's milestone G. The design now says how: registrations, the
catalog graph per watched branch, pipeline steps and their policy, a
session's own merge through merge-pull reading the head's latest run,
edges between artifacts, values of any size, the tools, the runner's kept
data and the build capacity, and the acceptance tests by level and
repository. Every section the pipelines replace keeps governing G's own
delivery and says so; the recovery merge points to its decision.

Claude-Session: https://claude.ai/code/session_01JG73KK8HD1gufnqyFxMFdW
2026-11-10 02:35:25 +01:00
mesh-tools 6611b94e3d Merge pull request 'Amend design 47 for milestone G: the gate's record, the seat git, the merge on the Approve alone' (#530) from design/47-G-amendment into main 2026-11-09 19:38:46 +00:00
mesh-tools fc6a30070f Merge pull request 'Developers, design writers and preparers can say a commit others wait on as it lands' (#531) from agents/developers-report into main 2026-11-09 19:37:47 +00:00
jschoubben c9abb1c5f9 Test 58 names a part not heard, not a node away: a check still passes a sleeping node with a warning
mesh/delivery merged: source.main moved to novox/hq@6611b94e3d7a
mesh/check checking c9abb1c5f92e on 6611b94e3d7a
The test read as failing every check while a node sleeps, against ADR 0593 §9 and the design's own try section.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-09 20:12:00 +01:00
jschoubben b3484cdcfc Name the indexer's mesh-wide hold among what milestone G retires and a revert restores
mesh/delivery closed: a newer head of the same pull request
mesh/check checking b3484cdcfc50 on 2de011e070d4
The per-node wait is the assigner's, so the indexer's hold states and tools go with its modules state.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-09 20:07:35 +01:00
jschoubben 76b317ed3a Take in round 3's second pass on design 47's G amendment: put-stands timed by the watchman, the put ends by done
A new step would read as a running put to a reverted assigner, the running-before-assigner order is no
edge of the module graph, put-stands needs a writer that survives a stopped assigner, and the tool
runner's revision rests on ADR 0663, now required before the push beside ADR 0661.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-09 20:06:40 +01:00
jschoubben 205992f15c Take in round 3's review of design 47's G amendment: the put's end by step, the readers' order and waits
The reviewers found the put's end written as built when the recovery reads the have, no end for a put of
the running build, nothing said for a put that stands, two of the eight readers outside the served-verb
order, the readers missing the no-view rule and the candidate case, and a revert's restored modules
unsaid. Each is now written as the lead ruled, with rows in tests 61 and 62.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-09 20:02:31 +01:00
jschoubben e73a1a7bcf Take in round 3 of design 47's G review: no module reads the indexer's modules, a put ends itself, ADR 0662 withdrawn
G stops declaring the indexer's modules, so the eight other modules that read it would break unless they
move to the catalog half in the same milestone. A put without hold never ended and pinned its want; ADR
0546 §5 already says it ends once the node runs it, now heard on the node-engine seat's running event.
ADR 0662 decided ADR 0661's switch another way and is withdrawn, so G builds ADR 0661 alone.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-09 19:56:42 +01:00
jschoubben 64cdada15a Developers, design writers and preparers can say a commit others wait on as it lands
mesh/delivery merged: source.main moved to novox/hq@fc6a30070f68
mesh/check being carried to 64cdada15a34 on fc6a30070f68 | from 2de011e070d4
Without SendMessage, milestone G's developers could report only at their end, so a seat definition the
others wait on could not be handed over mid-way.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-09 19:55:31 +01:00
jschoubben 4f388e8452 Take in round 2 of design 47's G review: running kept per subject, the merge waits on the Approve alone, ADR 0661 named
mesh/delivery closed: a newer head of the same pull request
mesh/check pass 4f388e845294 on 2de011e070d4 | unit 43 run, 0 kept, 1 skipped | integration 43 run, 0 kept, 1 skipped | composition passed in 85s on 4…
Round 2 found the pass's running events could age out of a new consumer's reach, the merge bullet still
holding for a carry, the checker's own owed acts unanswered, findings that never cleared, and
kept-artifact open to any module and caller. Each is now written as the lead ruled, with its test row.

Claude-Session: https://claude.ai/code/session_01NcbgfFfQ4Z9UE5jK32PmE5
2026-11-09 19:21:49 +01:00
jschoubben 2fd96e1e4e Take in round 1 of design 47's G review: hear what nodes run, say what the forge is owed, reach the forge by its seat
The reviewers found a seat's pass waiting on a call to a node that may be away, acts owed to the forge
that could be lost or done twice, a seat git that missed the gate's and half the parts' forge calls and
events, a merge that still judged a check the operator removed, and waits with no bound or condition.
The design now keeps what each node runs from its running events, makes every owed act safe to repeat
with its outcome said and recorded, gives the seat git every call and event with its act verbs granted
by the holder's callers, orders forge, bridge and gate with a way back, adds module-held and
check-could-not-run, numbers tests 56 to 60, and says how the store seat's holder takes its own build.

Claude-Session: https://claude.ai/code/session_01NcbgfFfQ4Z9UE5jK32PmE5
2026-11-09 19:21:49 +01:00
jschoubben 7f2f37d9e6 Amend design 47 for milestone G's prepare, so G is built from what the records hold
The prepare found the design behind its records and the code: Placement retired by ADR 0641 while the
design still put two seats in a contracts module never made, ADR 0659 and ADR 0660 not followed, the
checker and the forge bridge reading the gate's state, the delivery parts naming the forge module's
tools, and four hand-wired seat-list readers. Design 47 now writes each as the design's choice under the
record it follows; the hold and the merge turns are removed by the operator's word with their record
still to come, and a session's recovery merge waits for one. The glossary moves Placement's words into
Core and Delivery and adds the seat mesh-grants; the words test refuses Placement as a domain heading
(ADR 0641 §5); design 26 takes the new contracts homes and the seat git's verbs.

Claude-Session: https://claude.ai/code/session_01NcbgfFfQ4Z9UE5jK32PmE5
2026-11-09 19:21:49 +01:00
mesh-tools 2de011e070 Merge pull request 'Research writes its requirements from the operator's answers before any option' (#529) from docs/research-requirements into main 2026-11-09 16:21:08 +00:00
jschoubben d61bed820f Research writes its requirements from the operator's answers before any option
mesh/delivery merged: source.main moved to novox/hq@2de011e070d4
mesh/check checking d61bed820f8e on 2de011e070d4
The operator, 2026-11-09: a list of requirements shapes the research, so every
option is weighed against requirements the operator has corrected first.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-09 17:19:15 +01:00
mesh-tools 3cf28e840d Merge pull request 'Watcher definition: a report along the way does not end the watch' (#528) from agents/watcher-keeps-watching into main 2026-11-09 08:49:46 +00:00
jschoubben c57930a759 A watcher reports along the way with SendMessage and keeps watching; only the end, a failure, the bound or a twice-failed call ends it
mesh/delivery merged: source.main moved to novox/hq@3cf28e840de3
mesh/check checking c57930a75988 on 3cf28e840de3
On 2026-11-09 four watchers in one session ended at their first five-minute report,
leaving waits unwatched while a check, a take-in and an answer were still to come.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-09 09:31:22 +01:00
mesh-tools e7f090e333 Merge pull request 'Glossary names no seat's holder: who holds a seat is read from the mesh' (#527) from docs/glossary-no-holders into main 2026-11-08 23:21:13 +00:00
jschoubben dea6b197c5 Glossary names no seat's holder: who holds a seat is read from the mesh
mesh/delivery merged: source.main moved to novox/hq@e7f090e33302
Which module holds a seat is a fact of the mesh's assignments, and a copy
in the glossary drifts (the operator, 2026-11-09: "it shouldn't be in the
glossary"). Each entry keeps its definition and drops the holder; the head
says the holder is read with mesh-module-assigner.assignments and seats.
The words test now refuses a seat held or claimed by a named module; it
failed on twenty statements before they were reworded.

Claude-Session: https://claude.ai/code/session_01NcbgfFfQ4Z9UE5jK32PmE5
2026-11-09 00:20:26 +01:00
mesh-tools 1fb7943efd Merge pull request 'Agent definitions: no symbolic links; a review proves a fix against the live mesh (issue 1966)' (#526) from agents/developer-no-symlinks into main 2026-11-08 15:03:27 +00:00
jschoubben e01bf97a17 Reviewer definition: prove a fix against the live mesh, never a re-creation
mesh/delivery merged: source.main moved to novox/hq@1fb7943efdb0
mesh/check failed: ace: not composed on ace: the composer at the node-engine's trunk 875b4ee5ce38d4bb96089ba3b05962b450… | e01bf97a17f9 on 1fb7943efdb0
Twice on 2026-11-08 a review re-created the indexer's manifest validator without
its seat list and passed a fix the live one still refused (issue 1966); the
operator asked for this line in the same change.

Claude-Session: https://claude.ai/code/session_01NcbgfFfQ4Z9UE5jK32PmE5
2026-11-08 01:58:58 +01:00
jschoubben b6b657917e Developer definition: place siblings without symbolic links
mesh/delivery closed: a newer head of the same pull request
mesh/check failed: mesh-build-go: the change's manifests cannot be judged: the seat list is not read (the seat list is … | b6b657917ed6 on aaad9daa69bb
A developer placed a sibling worktree under /var/tmp with a symbolic link
(2026-11-08, issue 1966); the mesh's hard rules allow none anywhere.

Claude-Session: https://claude.ai/code/session_01NcbgfFfQ4Z9UE5jK32PmE5
2026-11-08 00:34:23 +01:00
mesh-tools aaad9daa69 Merge pull request 'Design 47's B0 says its one new seat verb is delivered first, alone (issue 1921)' (#525) from feat/b0-checking-program into main 2026-11-07 00:32:44 +00:00
jschoubben ec71485755 Design 47's B0 says its one new seat verb is delivered first, alone (issue 1921)
mesh/delivery merged: source.main moved to novox/hq@aaad9daa69bb
mesh/delivery-group group feat/b0-checking-program merged: every member is merged
mesh/check being carried to ec714857556d on aaad9daa69bb | from 885099eebbbd, mesh-catalog 980ade933853, mesh-delivery 2d201f781ced, mesh-sdk c1b50e99…
B0's first check failed because the running indexer did not know the checker
seat's verb input and the composition took its verdict over the check's own
validator. The operator chose to deliver that verb alone first (2026-11-07).

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-07 00:53:27 +01:00
mesh-tools 885099eebb Merge pull request 'Design 47's B0 says what was built: the checking program on every check, its input by the checker's verb (issue 1921)' (#524) from docs/47-b0-row into main 2026-11-06 23:01:03 +00:00
jschoubben d676466578 Design 47's B0 says the checker is delivered first and a failed seat list ends the check there (issue 1921, B0 review round 2)
mesh/delivery merged: source.main moved to novox/hq@885099eebbbd
mesh/check being carried to d676466578c4 on 885099eebbbd | from dd55153cf355
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-06 23:56:30 +01:00
jschoubben da828c147a Design 47's B0 takes in round 1's rulings: the input asked of the checker's verb, the cycle rule the program's
A build module reading the checker's state broke ADR 0596 §2, and no record names the
cycle rule as an exception to ADR 0656 §5; the code now follows both, and so does the
design, with the answer's bound that the build module now enforces.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-06 23:51:08 +01:00
jschoubben 35b92fde78 Design 47's B0 says what was built, so the review reads the design the code follows
The first code review round found the B0 section described the input and answer in the
build seat's objects, no running rule after B0, and a carry's program built at merged heads.
It now says the input is kept in the checker's own state and the answer rides the step key,
names the cycle rule and a failed program's wording as the two exceptions until milestone G
with why, and says where a carry's program is built and when a trunk move asks a new build.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-06 23:23:53 +01:00
jschoubben 60369fddee Design 47's B0 takes the lead's rulings on its open points
The begin bound of a run ask, the referenced objects' size, the kept
programs' tools and their 30-day sweep, and the versioned input were left
open by the first amendment; each is now one of B0's choices with its
reason, as ruled, so the developers and the review read one text.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-06 22:10:14 +01:00
jschoubben 12489d580c Design 47's B0 names the trunk's homes and runs the checking program on every check
Row B0 named mesh-delivery-contracts for the ask `run`, but the registry group
creates that module and joins milestone G, after B0; on every trunk before G
the seat's types are mesh-sdk's go/delivery and go/composer, which G's merge
moves (ADR 0634 §4, ADR 0635 §4). The bootstrap section now says every check
runs the checking program (a lone pull request a group of one, a carried
verdict included), that the program makes the heads' seat list, and carries
the seams review's three findings on B0's prepare as the design's own choices
with their reasons: a dropped `run` ask fails by name within a bound, kept
programs have tools and a sweep, and the input goes by reference with a size
bound.

The glossary gains "seat list", used as a term by ADR 0635, ADR 0636 and
design 47 without an entry, retiring "seat set" in hq; four designs take the
word, and issue 299's narrative keeps its day's word by an allowance row.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-06 22:08:31 +01:00
mesh-tools dd55153cf3 Merge pull request 'Design 47 follows ADR 0656 and ADR 0644: the delivery's own parts checked and delivered by their own change (issue 1921)' (#523) from docs/47-two-graphs into main 2026-11-06 20:42:02 +00:00
jschoubben 27d31d5490 Merge remote-tracking branch 'origin/main' into docs/47-two-graphs
mesh/delivery merged: source.main moved to novox/hq@dd55153cf355
mesh/check being carried to 27d31d54908f on dd55153cf355 | from 5ff1298e2173
2026-11-06 21:37:41 +01:00
jschoubben 8a85a2fd54 Design 47 records its review by architectural domain of 2026-11-06
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-06 21:37:41 +01:00
jschoubben 49ecfb6cdf Design 47 keeps the catch-up tool in the restore point
At a revert the manifest being applied is the one from before the change,
which names no catch-up tool, so the node agent could not find it there. The
newer build's manifest declares it beside the kept data whose form it changes,
the backup records it in the restore point, and the node agent takes it from
there.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-06 21:37:09 +01:00
jschoubben 25d9f8df61 Design 47 names the forms milestone G changes and how a revert catches up, read from the code
Round 3 found the revert section left open which kept data G changes, the order
a revert needs, and whether the parts of before can catch up. Read on main, the
indexer catches up from its forge consumer, whose stream keeps only 7 days, so
the restore also runs its existing take-in of every repository; the assigner
keeps nothing to restore and computes the wants again; the gate's start reads
the forge, the pending answers and the source again. The wants keep their form
and are never restored, since that would send nodes to older builds.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-06 21:35:44 +01:00
jschoubben af6695161c Design 47 writes only the new forms, and reverts by restoring the parts' kept data
The operator looks only forward, yet wants reverts supported. So the indexer,
the assigner and the gate no longer write the old keys, wants source or merges
and turns beside the new forms, and milestone H goes. A revert is delivered like
any change: ADR 0404's backup covers only the items on a module's node, so the
delivery system gains a restore point of a module's kept data on the bus,
taken before a change of its form and restored beside the live data before a
reverted build starts, after which the part catches up forward; a missing
restore point holds the revert and asks the operator.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-06 21:30:35 +01:00
jschoubben ce3fe6aa3b Design 47 takes in round 2: waits timed from their start, composing held per node by the assigner, stores sized
Round 2 found a wait whose holder stopped could go untimed, a node away holding
others' wants, a new field in the want that an old node-engine cannot read,
stores sized for whole revisions, a full store that never cleared, a ledger
sized without its operations, and a kept build a program could rewrite. The
design now says wait-held at a wait's start with its bound, starts each clock
when the new version's build is done, holds a node's wants in the assigner
until that node's new node-engine runs, stores an unchanged entry once, sizes
the ledger from its operations, clears full on room, keeps only builds from a
run step that runs nothing else, names every change of the checking program's
fingerprint, and makes the switch safe to run twice.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-06 21:23:25 +01:00
jschoubben de7358a2c5 Design 47 takes in round 1 of the review by architectural domain
The reviewers found the amendment left the stores to grow without a bound,
the waits without an owner's tools or events, a rollback that would send nodes
back to old builds, a newer indexer that could never be wanted, events any
account could forge, and the node-engine reading every node's placements. The
design now chooses, each with its reason: a default record retention and caps
with fill conditions, a revision removed when no record pins it and it is not
current, waits held and released by each step's own part with their events,
the build module's wait ending on the agreed toolchain, the old keys kept
written until a named milestone H for a rollback, the running indexer making
the newer indexer's entry, rule changes named by the running checker from
outside the sandbox, and subjects and grants that let each part say only its
own. The review verb leaves milestone G for issue 1782, and the watcher's
definition goes back to main, since it contradicted an approved instruction.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-06 21:18:14 +01:00
jschoubben 8d98d54bd2 Design 47 tests each change ADR 0656 makes, and leaves the review verb as ADR 0493 §5 has it
ADR 0656 v6 no longer changes ADR 0493 §5: who may review is another question,
waiting on issue 1782, so the design stops saying the reviewer comes from the
caller's bus account or that the author is refused. Each change ADR 0656 makes
in an earlier record now has its own acceptance test, so the milestone proves
every one of them.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-06 20:57:46 +01:00
jschoubben dbc047bf56 Design 47 follows ADR 0656 alone and writes its own choices with their reasons
The operator ruled that research 085 graduates into one record per question:
ADR 0656 holds the whole answer and supersedes ADR 0645 to 0648, and the short
records 0649 to 0655, 0657 and 0658 are withdrawn. The design now cites ADR 0656
or ADR 0644 for what they decide, and what they leave is no longer an open
point but this design's choice, listed with its reason, so the review can
judge each one.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-06 20:56:35 +01:00
mesh-tools 5ff1298e21 Merge pull request 'Playbooks 01 and 02: one record per question an effort answers; design choices live in the design' (#522) from docs/one-record-per-question into main 2026-11-06 19:55:23 +00:00
jschoubben c1cf0149b6 Design writer and hq-graduate follow the new step 2; the playbook names no record by number
mesh/delivery merged: source.main moved to novox/hq@5ff1298e2173
mesh/check being carried to c1cf0149b6d9 on 5ff1298e2173 | from 1ca5a3a1331f
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-06 20:52:38 +01:00
jschoubben 5e3b836404 Playbooks 01 and 02: one record per question an effort answers; design choices live in the design
Research 085 graduated into eleven records where it answered two questions:
the rule 'one record per choice that could be reversed on its own' made
every design choice a record of its own.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-06 20:51:00 +01:00
jschoubben cad2693361 Design 47 carries what research 085's records no longer say, and cites the short records
The operator ruled that a decision record is a question, its options and the
choice made, so research 085's records were cut to their choices: ADR 0656,
0657 and 0658 supersede ADR 0645, 0647 and 0648, and ADR 0652 to 0655 split
ADR 0646. What the long forms carried beyond the choice now lives here as
design text and acceptance tests: the graphs' keeping in the old objects
sections and their retention by the gate's pin and free events, the
candidate's graphs, the step events' fields, the first tier and the sandbox,
the trunk's validator beside the change's own, the waits' holders and tools,
the delivery record's consumer, switch, rollback, node away, small fix and the
Approve held at its heads, the milestone ask and events, and the trace's link
kinds. Each fact cites the record that decides it, or says it is this
design's, so the review can tell the two apart.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-06 20:33:24 +01:00
jschoubben e99094ab94 Design 47 and the glossary follow ADR 0644 to ADR 0651: two graphs, one revision view (issue 1921)
Research 085 found that each acting part handled a change to itself by a case
of its own, and that parts disagreed on the state they read: 7 of 24 failures
since 2026-11-01. Design 47 now describes the catalog graph and the computed
assignment graph kept per revision, a change delivered as its graph
difference, acting parts' rules on their seats as bounded waits, the check on
the change's own acting parts, the revision view, the gate's delivery record
and the trace's links, with the bootstrap delivery before that milestone.

It also settles what the records leave to this amendment, as proposals for
the review: the acting rule's fields and mesh-sdk cost, the fingerprint
handover, the build seat's ask that runs a program in the sandbox, the first
tier's bound, the step events' subjects, and the tools the seams review
listed. The glossary gains the seven words of research 085's Domains. The
watcher's definition learns that a quiet five minutes is no reason to end,
since its final report is its only channel.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-06 19:41:16 +01:00
mesh-tools 1ca5a3a133 Merge pull request 'Design 47 follows ADR 0633, 0637, 0638 and 0639: the take-in by the indexer it names, and the group try (issues 1966, 1921)' (#521) from docs/47-try-and-take-in into main 2026-11-05 20:06:31 +00:00
jschoubben b926db338d Design 47 cites ADR 0639 §9 for the hand drop, so both hand acts are said to clear take-in-waits
mesh/delivery merged: source.main moved to novox/hq@1ca5a3a1331f
mesh/check being carried to b926db338dcf on 1ca5a3a1331f | from 674f88f9abb8
ADR 0639 v9 gives the drop its own clause and moves the checks to clause 10.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 20:26:55 +01:00
jschoubben d61e18de41 Design 47 cites ADR 0639 for the review's rulings, and fixes the indexer's build at its start
The rulings of round 1 now rest on a record, clause by clause, instead of on the
review; the indexer's own build is set once from the build it was started with, so a
process being replaced cannot take in with its old manifest validator (ADR 0639 §7).

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 18:46:11 +01:00
jschoubben 8b923e6bff Design 47 takes in round 1 of the review by architectural domain
The try and the take-in described seams, failure paths, live cases and the health
chain less exactly than the release builds them: the reads that stay until issue
1966, the bus form of pending and the tryevent contract, the try's tools against the
four lines, a checker_tries that does not answer, try events never lost, what a
narrowed try keeps, the finding and the condition keeper, and both live cases. Rulings
no record decides are marked as the review's.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 18:38:05 +01:00
jschoubben 26b6dcf4f6 Design 47 cites ADR 0638 and its recovery order, so a try's candidate survives an indexer restart
ADR 0638 makes the candidate from the checker's events and decides that the indexer
creates its consumer, asks checker_tries and fixes the candidates before it handles
an event; the design now says so and how it is checked.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 18:32:49 +01:00
jschoubben 5688a2efa1 Design 47 names the try's events, so the indexer's path to a try is a stream it consumes
The checker says a whole try's start and end as try.started and try.ended on its stream
checks, and the indexer recovers open tries through checker_tries; the record changing
ADR 0637 §4 carries this and is cited until its number is known.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 18:31:21 +01:00
jschoubben 7e33e9a15f Design 47 follows ADR 0637 and ADR 0633: a group try, and a revision taken in by its own indexer
A try now checks a delivery group at exact commits with the check's own code, and a
merge's take-in waits for the indexer its revision names, so design 47 says both where
its checks and its source revision are described. The indexer hears a try on the
checker's events rather than reading its state (ADR 0596); the record changing
ADR 0637 §4 to say so is to follow. The glossary gains try and pending revision.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 18:23:09 +01:00
mesh-tools 674f88f9ab Merge pull request 'Designs and glossary follow ADR 0634, 0635 and 0636: shared code lives with its owner, the seat list from the bus (issue 1966)' (#520) from docs/domain-sdk into main 2026-11-05 13:01:50 +00:00
jschoubben b81c05c36e Designs follow ADR 0636 as accepted: a waiting reader, the bootstrapped three
mesh/delivery merged: source.main moved to novox/hq@674f88f9abb8
mesh/check being carried to b81c05c36e95 on 674f88f9abb8 | from 160f384e4fcd
The accepted record renumbers its clauses and settles what the review asked: a
reader without the seat list starts nothing new but keeps running, its wait
reaches the operator through the condition keeper and the router, which are
bootstrapped with mesh-catalog, a missing definition blocks new claims only,
a move of a definition is a handover, and the private network's value is the
seat's contract. The renames also refuse old names in consumes and say what an
old installer meets; design 47's done milestones keep the name they shipped.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-05 13:40:32 +01:00
mesh-tools 160f384e4f Merge pull request 'Watcher definition: what a machine runs is read from the machine (issue 1585)' (#519) from agents/watcher-reads-machines into main 2026-11-05 12:28:43 +00:00
jschoubben e1713bf59a Designs follow ADR 0636 v4: the seat-definitions verb, the indexer bootstrapped
A full restart must bring back the part that serves the seat list, so the
bootstrapped definitions now name mesh-catalog beside the foundation's seats,
with a test of it. A seat defined outside the mesh's own contracts is taken
from its own contracts module, not refused. The bus server's resources and the
placement check get their owners, and two stale seat names in design 26's old
table are corrected.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-05 13:22:52 +01:00
jschoubben cb1b055b80 Designs follow ADR 0636 and the first review's rulings
The review by architectural domain found that readers of the seat list would
name the indexer, never hear a removal, could deadlock a full restart, and
that any folder could define a seat again; the-private-network's holder sat
in mesh-sdk. ADR 0636 and the lead's rulings answer these, and the designs
must say so before develop.

- design 26: the seat list through a verb and two events of mesh-catalog; a
  reader without it acts on nothing new; a seat defined only in its domain's
  contracts module; states are any holder's, caller-written only for events
  and logs, each with its size cap, declared in the holder's requires; the
  renames move no state; the group's check reads every member's head;
  Connectivity in the mesh-networking repository, held by mesh-wireguard;
  acceptance tests by level and repository; two stale table rows corrected
- design 47: the composer in mesh-delivery-sdk, the bus composer's seat
  parts in the grants part, placement's contract and check by their seat
- design 20: a module's own package is fingerprinted by its own files
- glossary: mesh-placement retired for tool descriptions too; the
  composition renamed to mesh-delivery-sdk's; six seats given their domains

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-05 13:20:45 +01:00
jschoubben c70b275187 Watcher definition: the status view, not the delivery view; the assigner's want verb named (review round 1)
mesh/delivery merged: source.main moved to novox/hq@160f384e4fcd
mesh/check being carried to c70b27518712 on 160f384e4fcd | from d1e2059dab3f
Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-05 12:55:21 +01:00
jschoubben b165eb9438 Watcher definition: what a machine runs is read from the machine, not the delivery view's take-in line (issue 1585)
A watcher of 2026-11-05 reported that four modules taken in by hand reached no machine, from the view's
'not yet taken in' line; node-engine.composition showed all four running their new builds.

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-05 12:54:10 +01:00
jschoubben 7d77102db0 Designs and glossary follow ADR 0634 and ADR 0635: shared code lives with its owner
The registry milestone now moves the Delivery domain out of mesh-sdk, creates a
contracts module per domain it moves and the first domain SDK, defines 25 seats
whole with their states, reads the seat list from the bus and renames two seats.
Design 26's *Where a seat's code goes* is what the review and the developer read
to place every new name, so it must say this before develop starts.

- design 26: definitions carry states; the 25 seats by contracts module; the
  seat list from the bus; the two renames; the domain SDK and a module's own
  package; the transfer in the Delivery domain's SDK; checks per ADR 0634 §10
  and ADR 0635 §6
- design 20, playbook 06: the homes of shared code, the first that fits
- design 47: the seat mesh-module-assigner
- glossary: domain SDK added; seat-set package retired; mesh-module-assigner,
  mesh-wants, mesh-node-enroller and the-private-network in their domains
- preparer and code reviewer definitions: the homes, without the retired word

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-05 12:46:06 +01:00
mesh-tools d1e2059dab Merge pull request 'Developer definition: the manifest validator gets one predefined key list for both cases, always (issue 1585)' (#518) from agents/validator-key-list into main 2026-11-05 01:28:03 +00:00
jschoubben 304d17b561 Developer definition: the manifest validator always gets a predefined key list, as the checker passes one
mesh/delivery merged: source.main moved to novox/hq@d1e2059dab3f
mesh/check being carried to 304d17b5612c on d1e2059dab3f | from 331e5707922b
The review of hq #517 noted that the checker always passes the predefined key list (the change's when it alters it,
else the trunk's), while the text said only "when the change alters it" (issue 1585).

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-05 02:15:45 +01:00
mesh-tools 331e570792 Merge pull request 'Developer and reviewer definitions: validate every manifest a change touches before handing back (issue 1585)' (#517) from agents/dev-validates-manifests into main 2026-11-05 00:44:47 +00:00
jschoubben 749249466f Developer definition: the validator run as the checker does it (review round 1)
mesh/delivery merged: source.main moved to novox/hq@331e5707922b
mesh/check being carried to 749249466f8c on 331e5707922b | from 6b8b6b08e39a
Built at mesh-delivery's trunk unless the change alters the validator; raw
manifests in the two cases change and trunk; every manifest whose verdict
differs is reported, an unchanged one included (ADR 0579 §2).

Issue 1585.

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-05 01:41:12 +01:00
jschoubben c4b98b2a03 Developer and reviewer definitions: validate every changed manifest before handing back
The take-in group's first check failed after a full build on a manifest the
validator refused (an indexer's own stream outside the bus's forms); no local
run had validated the changed manifests. The developer now runs the manifest
validator as the checker does and reports each verdict; the reviewer asks for it.

Issue 1585.

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-05 01:38:40 +01:00
mesh-tools 6b8b6b08e3 Merge pull request 'Say manifest validator, not judge: the glossary gains the word and retires the noun' (#516) from docs/manifest-validator into main 2026-11-04 22:39:51 +00:00
jschoubben f91a5a7c75 Name the composing program built from a change, a probe for one health check, and ADR 0579 §2's whole case (review 1)
mesh/delivery merged: source.main moved to novox/hq@6b8b6b08e39a
mesh/check being carried to f91a5a7c75e5 on 6b8b6b08e39a | from f0c05d1edb84
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 23:37:21 +01:00
jschoubben aac8494ca3 Say manifest validator, not judge: the operator asked why a new word was needed for the indexer's rules
The glossary gets the entry in the Delivery domain with the records that decide it, and retires the noun in
hq's prose on a Not line; the verb stays. Where "judge" named something else, the design now uses that
thing's word: the node-engine's look at health, an account or the network; the composition check built from
a change; the claude-code module's judgement tool. Five issue narratives keep the word of their day.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 23:32:13 +01:00
mesh-tools f0c05d1edb Merge pull request 'Where new code goes is a design question: playbook 08, the developer and the code reviewer check it against designs 20 and 26' (#515) from docs/placement-is-a-design-question into main 2026-11-04 22:23:45 +00:00
jschoubben b525f1059b Name the design section, not its number; the preparer and playbook 07 place new code the same way (review 1)
mesh/delivery merged: source.main moved to novox/hq@f0c05d1edb84
mesh/check being carried to b525f1059b69 on f0c05d1edb84 | from d4ae72782336
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 23:20:38 +01:00
jschoubben 69f95f551d Where new code goes is a design question: the playbook, the developer and the reviewer check it against designs 20 and 26
mesh/delivery closed: a newer head of the same pull request
mesh/check pass 69f95f551d19 on d4ae72782336 | unit 39 run, 0 kept, 1 skipped | integration 39 run, 0 kept, 1 skipped | composition passed in 17s on 4…
A small fix's lead placed a seat's type in mesh-sdk by its own handover (issue 1748), against design 26;
the developer built the handover and the reviewer checked against it, so it passed review and its check.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 23:17:57 +01:00
mesh-tools d4ae727823 Merge pull request 'Designs 20, 25, 26, 32, 51, 55 and playbook 07: the registry milestone as built (issue 1966)' (#514) from docs/registry-milestone-designs into main 2026-11-04 18:19:24 +00:00
jschoubben 304ce84cac Designs 32, 25: a state may be read-only to the tools; nats_requests reads spent so
mesh/delivery merged: source.main moved to novox/hq@d4ae72782336
mesh/check being carried to 304ce84cac18 on d4ae72782336 | from 273d608d9faa
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 18:10:48 +01:00
jschoubben e0a70a468b Designs 25, 51: name the spent state; a shared-digest tag is recorded as kept
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 18:08:58 +01:00
jschoubben 571c0a3e68 Designs 25, 51: follow review round 2 (decided via uses, spend record apart, fix checks, genesis gap, kept answer)
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 18:08:36 +01:00
jschoubben b0a9fa14da Playbook 07: a group's pin is for GOWORK=off; the check builds against the group's heads (ADR 0573), so an older pin is not refused
The line said the checker refuses an older commit of the same branch; that refusal was removed as a fault of the check
(issue 1585). Found pinning the registry milestone, where mesh-sdk and mesh-contracts import each other.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 17:49:32 +01:00
jschoubben b3a9970ac7 Design 51: name ADR 0627 among its decisions
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 17:33:15 +01:00
jschoubben 5499c3546b Design 51: cite ADR 0627 for the holders' shared receiving side, ADR 0611 §3 for the drop
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 17:32:54 +01:00
jschoubben c3d8367b33 Designs 25, 51: write what review round 1 settled (acted key, ask lifetime, loopback bootstrap, shared idle drop, shared-digest tag, forge account removal)
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 17:32:21 +01:00
jschoubben 8f75606167 Designs 25, 51: follow review round 1 of the registry milestone
Design 25 counts seven first-view tools, the indexer's graph granted to the
assigner alone (ADR 0631); the bus module's process wakes on its own decided
and acts on a warrant once (ADR 0259 §6, ADR 0620 §2) instead of a timer, and
the tools watch their request key; §12.7 carries the rows, and its first-view
row, split by an earlier edit, is one row again. Design 51's incoming store
asks the objects template's most, 8 GiB, with no lifetime; the holder drops an
idle transfer itself (ADR 0611 §3).

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 16:23:11 +01:00
jschoubben 901c92a924 mesh-developer: run the repository's own checks whole before reporting a head, and start no agents of your own
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 15:51:25 +01:00
jschoubben bdc5ae4fe6 Design 26: the seat definitions' generator is one program in the Go build module, run by both build modules
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 15:09:57 +01:00
jschoubben aced03a287 Design 32: a package is named by its name in its own language, one name across the indexer, builder, registry seat and users
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 14:46:30 +01:00
jschoubben da1c7f30a9 Designs 25, 26, 20, playbook 06, glossary: follow ADR 0625 to 0627 for the registry milestone
Design 25 § 12.3 now says how the bus module's process really receives a person's
request (its own state requests, the warrant verified by the process itself), the
process account with keeps it holds beside the snapshot account, and that
mesh-broker's events go on the shared stream like every seat's, with the 30-day
history its own stream. Design 26 gains where a seat's code goes: a package per seat,
the seat-set package in mesh-contracts, the seven client packages and the
seat-neutral parts transfer; design 20 and playbook 06 tell a module's author the
same, and the glossary gains client package and seat-set package. mesh-developer
stops at two thirds of its context, as the milestone's agents learned.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 13:19:57 +01:00
mesh-tools 273d608d9f Merge pull request 'Designs 51, 32: the windows wake on the node-engine seat's manifest-reads-changed (ADR 0618), and every build stays readable by every counted node-engine (ADR 0624)' (#513) from docs/cite-adr-0618 into main 2026-11-04 09:08:19 +00:00
jschoubben cac55dc254 Design 51: follow ADR 0624, every build keeps each counted node-engine's form
mesh/delivery merged: source.main moved to novox/hq@273d608d9faa
mesh/check being carried to cac55dc2543a on 273d608d9faa | from 6cd965b3cdf6
ADR 0624 supersedes ADR 0623, which could find the node-engine's own
builds only by the module's name. Every build now keeps each form a
counted node-engine states, the oldest in the single reference, and a
node is held only when it reads none of them.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 09:59:44 +01:00
jschoubben 01d4f2781e Design 51: keep the node-engine's own build readable by away nodes (ADR 0623)
mesh/delivery closed: a newer head of the same pull request
mesh/check pass 01d4f2781ef0 on 6cd965b3cdf6 | unit 39 run, 0 kept, 1 skipped | integration 39 run, 0 kept, 1 skipped | composition passed in 33s on 4…
Under ADR 0618 alone a node away with an old node-engine would be held
from the node-engine's own build in the new form and could never update
itself. ADR 0623 keeps every counted node-engine's form in those builds
and moves them to a node that reads any one of them.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 09:48:56 +01:00
jschoubben 48fa0a5a48 Designs 51, 32: name the manifest-reads grant and who holds the old-form window
The upgrade and security review asked that each window's caller be
granted the event through its invokes, that an away node be held by the
assigner rather than the old-form window, and that a node-engine not yet
serving manifest-reads keep a window open by name.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 09:46:46 +01:00
jschoubben f47744a8b6 Designs 51, 32: cite ADR 0618 for the windows' wake and the reference form
ADR 0618 is accepted: the node-engine seat's manifest-reads verb and its
manifest-reads-changed event wake the old seat name's, the old-form and
the rename windows, and the builder writes a new form only once every
present node-engine states it. The next delivered stays only for the
rename window's case the record does not cover.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 09:44:31 +01:00
mesh-tools 6cd965b3cd Merge pull request 'Designs 25, 51: the registry milestone's prepare points (a failed pull retried on the store holder's settled, the owners' read tools, verdicts as a first view, ADR 0622)' (#512) from docs/registry-prepare-points into main 2026-11-04 08:40:09 +00:00
jschoubben 4f6b9f0ca5 Designs 25, 51: bound the race retry to a settled heard after the pull started
mesh/delivery merged: source.main moved to novox/hq@6cd965b3cdf6
mesh/check being carried to 4f6b9f0ca5ff on 6cd965b3cdf6 | from e7ac664afff7
A retry on any settled ever heard would apply again on every failure; the
pull's start bounds it to one retry per settled, kept in memory. The seat and
holder beside a routed image are outside a container's digest, so naming
them replaces nothing, and the verdicts gap case is named.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 02:16:03 +01:00
jschoubben a838b6b3d3 Design 51: a routed image names its seat and holder, so the retry matches settled by it
The declaration carried only the store's address, so the node-engine could
not tell whose settled ends a failed pull without naming a module.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 02:13:10 +01:00
jschoubben 6c6b2c5cc1 Designs 25, 51: verdicts a first view of its own; every node hears the holder settle
The store's holder runs on one node, so a node-engine elsewhere must hear its
settled too, and a pull failed just after it settled must not wait. The
grants part's verdicts read is a first view under ADR 0622, not an ordinary
read tool, and the node-engine's reads granted in code are said, so the
live count of zero declared reads is not taken for more than it is.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 02:12:38 +01:00
jschoubben fb7aa0915f Design 25: the grants part reads the indexer's verdicts through a read tool
The grants part is the one reader of the indexer's verdicts, and no first
view covered that bucket, so its move had no path. It now takes its first
view through indexer_read_verdicts and follows the manifest events.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 02:05:51 +01:00
jschoubben 74b69857a1 Designs 25, 51: retry a swapped pull on settled; owners' read tools get names of their own
Neither condition-cleared nor delivered comes during the store holder's own
delivery, so the node-engine retries on the status view's settled for that
holder, heard through the grants part's list, and at its own start. The
status view keeps its tools and answers them through the owners' new read
tools, so no name moves between modules.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 02:04:51 +01:00
jschoubben 0e8a698025 Designs 25, 51: the owners take the view's six tool names; distribution's delivered retries a failed pull
The status view can no longer answer its six tools named for the owners'
buckets once it stops reading them, so the owners' tools take the names. A
finding held during distribution's own delivery raises no condition and so
no clear, so the node-engine also tries a failed pull again on the status
view's delivered for distribution, and nothing waits for a hand compose.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 01:59:59 +01:00
jschoubben 01d2ea0855 Designs 25, 51: settle the registry milestone's prepare points
A pull that falls in distribution's swap is tried again on the condition
keeper's clear of the store's holder's condition, so the delivery needs no
hand compose and no timer. The owners' tools for the other readers' buckets
are named, the reads count has its tool, and the test paths match main.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 01:58:54 +01:00
mesh-tools e7ac664aff Merge pull request 'Designs 25, 26, 32, 51, 55: the registry milestone's designs after the review by architectural domain (ADR 0615-0621)' (#511) from docs/registry-milestone-designs into main 2026-11-04 00:46:48 +00:00
jschoubben 457ac2eabd Designs 25, 26, 51: cite ADR 0620 and ADR 0621; the owners serve their first views as tools in a runtime of their own
mesh/delivery merged: source.main moved to novox/hq@e7ac664afff7
mesh/check being carried to 457ac2eabdcd on e7ac664afff7 | from 276cbef0c476
ADR 0621 keeps the manifest form as it is, so the process-subject form, its mesh-sdk change, the
open point on who grants it and the push's wait on a judge release all go; the per-verb grants and
the record's four checks take their place.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 01:43:36 +01:00
jschoubben 216e0e5102 Design 26: keep the seats' own events apart from their holders', since no event moves subject in this milestone
The four seats defined whole list only what their seat subject carries as the seat's; the
holders' events stay under their own subjects, checked by a fixture from the running holder.
The vault seat drops the two verbs no holder serves.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 00:53:30 +01:00
jschoubben f91f4879fc Design 25: cite ADR 0619, accepted, for the foundation that keeps the old sections
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 00:49:42 +01:00
jschoubben c8e6eb5edf Designs 26, 51, 25: define whole the four seats the milestone adds verbs or events to
The operator chose to move them now (2026-11-04), so a seat's new verb never lands at the seat
list's grain. Each is listed with what its holder's manifest on the trunk serves today; the
builder and the indexer hold no seat, so their events stay module events.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 00:47:45 +01:00
jschoubben a81334b1af Design 32: the rename window closes on the next delivered while ADR 0618 is only reserved, and no waiter on the vault's rotation wakes on own-retired
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 00:46:40 +01:00
jschoubben 546a18a20e Design 25: answer the third review (views families on the old streams sections under a record extending ADR 0595 §10, the tool runner's first-view tools on the mesh MCP server, the running judge takes the process-subject form in)
The assigner must start on a fresh mesh before the bus module can answer, so its new stream
stays on its old section; a new indexer can never judge its own manifest, so the running judge
must accept the form first. The glossary stops counting the mesh MCP server's tools.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 00:46:25 +01:00
jschoubben a9745b63cb Design 51: answer the third review (ADR 0618 only reserved, a module's images pulled before its containers are replaced, public package reads kept, levels on every new case)
The windows close on the next delivered until ADR 0618 is accepted; distribution's first
delivery needs the node-engine to pull every image first, since its registry serves every pull;
the operator kept public reads, so ADR 0346 stands and the route refusal goes.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 00:42:35 +01:00
jschoubben 021fca5e6d Design 32: the rename window wakes on the node-engine seat's event of ADR 0618, as design 51's old seat name does
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 00:32:24 +01:00
jschoubben fa2d199b16 Design 51: answer the second review (event wakes for every floor source, versions within the state cap, the direct-write and old-form windows shaped as the old name's, release on the caller list, distribution's own family)
Also writes the operator's answer of 2026-11-04 on anonymous reads (mesh network only, the public route refusing
the forge's package paths), the expected count of three builds per module, and the revert's order by the graph.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 00:31:30 +01:00
jschoubben cdfddff72e Design 25 § 12: owners' events carry the value whole, a deleted module's entries become orphans, the first views get named tools and a live bound
Answers the second review's V1-V9 by architectural domain: the assigner's views in the new form, newer by revision
alone, what a reader does when a recreate takes revisions back, a hand re-say of manifest-accepted, and the judge
question under the open point on issue 1966.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 00:29:30 +01:00
jschoubben 939d000327 Design 32: name the vault's event for retire and its family
Answers the second review's R1 by architectural domain: own-retired on the module events stream under the vault's subjects.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 00:28:07 +01:00
jschoubben e51e6961e3 Design 55 § 11: say what from: machine gives the page's port, that local processes reach it until issue 1968, and that the route proxy runs on its node
Answers the second review's K1 by architectural domain.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 00:28:07 +01:00
jschoubben 0168a545c5 Design 51: cite ADR 0617 for the unnumbered-first image and hold; narrow hold to what the sweep keeps, never an image
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 00:12:24 +01:00
jschoubben 0ae7fc2e57 hq-design-writer: it calls no verb that writes on the mesh; what a record must carry goes to the caller
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 00:11:23 +01:00
jschoubben da823becbe Design 55 § 11: answer the first review (the router's stream kept and capped, the page's port reached only by the route proxy, a re-read tool per source)
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 00:11:15 +01:00
jschoubben 3b1278b3bc Design 32: answer the first review (the checker invokes renames and waits for it, retire's caller list and warrant, retired events, tests, the away node's window)
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 00:10:57 +01:00
jschoubben 4710fd18c6 Design 26: answer the first review (the seat list's grain as ADR 0613 has it; out of step cleared only by the same contracts module)
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 00:10:40 +01:00
jschoubben 34384b6c9c Design 25 § 12: answer the first review of the milestone amendment (the bus module's first view against replays and an unanswered tool, the foundation readers' bounds, families and tools, the tests they join, the open grant)
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 00:10:19 +01:00
jschoubben 2d48b85c0e Design 51: answer the first review of the milestone amendment (builds counted from the records, the write side's window and outage, the revert's open points, holds and the old name's events, live proof)
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 00:09:56 +01:00
jschoubben 01a0a3a4d9 Amend designs 51, 26, 32, 25 and 55 for the registry milestone
The registry move, seat definitions, declares, bus state and the knowledge base
ship as one milestone (ADR 0612, 0613), ordered by the module graph's edges
(ADR 0614), with the foundation parts' first view (ADR 0615) and the overview
page (ADR 0616). The designs said four deliveries, a seat-definition source
nobody serves, a checker naming mesh-config, and left the bus module's first
view, caller and deletion paths open; they now say what the milestone builds.
Adds the glossary words it uses, playbook 02's cites link for an abandoned
effort, and the hq-design-writer agent definition.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 00:02:43 +01:00
mesh-tools 276cbef0c4 Merge pull request 'Graduate research 081: registries are seats published into over the bus, the builder numbers every build, seat definitions on the bus, contracts modules' (#510) from docs/graduate-081 into main 2026-11-03 20:12:24 +00:00
jschoubben 5b2cead5e3 Designs 51 and 32: answer the third review (publish-part listed with its callers, the node-engines' reports through the status view, C24 tested)
mesh/delivery merged: source.main moved to novox/hq@276cbef0c476
mesh/check pass 5b2cead5e3bc on 276cbef0c476 | carried from f6bd4246b0d7 | unit 39 run, 0 kept, 1 skipped | integration 39 run, 0 kept, 1 skipped | co…
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-03 18:10:19 +01:00
jschoubben 9845b78e5b Designs 51, 26, 32: answer the second review by architectural domain (ADR 0611, caller identity, node-engine reports, families and retention, Go references); add each design's review record
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-03 18:08:00 +01:00
jschoubben 20157fcafe Designs 51, 26, 32: answer the first review by architectural domain (ADR 0596 paths, holder-side caller lists, the file in the publish verb, findings for the condition keeper, windows on node-engine reports)
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-03 18:02:09 +01:00
jschoubben 27063a492b Graduate research 081 into designs 51, 26 and 32: registries are seats published into over the bus, the builder numbers every build, seat definitions on the bus, contracts modules (ADR 0605-0609) 2026-11-03 17:10:36 +01:00
mesh-tools f6bd4246b0 Merge pull request 'mesh-watcher waits on the event of the part that owns the fact it watches' (#509) from docs/watcher-owner-event into main 2026-11-03 13:27:21 +00:00
jschoubben 33d25c5074 mesh-watcher: the glossary's words, the delivery gate and the status view
mesh/delivery merged: source.main moved to novox/hq@f6bd4246b0d7
mesh/check pass 33d25c50742e on f6bd4246b0d7 | carried from 979bf3f19cce | unit 39 run, 0 kept, 1 skipped | integration 39 run, 0 kept, 1 skipped | co…
2026-11-03 13:29:36 +01:00
jschoubben bf5ef24ea3 mesh-watcher waits on the event of the part that owns the fact it watches (the operator, 2026-11-03) 2026-11-03 13:28:44 +01:00
mesh-tools 979bf3f19c Merge pull request 'hq ignores .work/, where every subagent's worktrees and context files live' (#508) from docs/groomer-and-gitignore into main 2026-11-03 11:58:08 +00:00
jschoubben c5644e4a5d The groomer is the issue tracker's, not an hq agent (the operator, 2026-11-03): only .work/ ignored stays
mesh/delivery merged: source.main moved to novox/hq@979bf3f19cce
mesh/check pass c5644e4a5de3 on f6bd4246b0d7 | carried from 979bf3f19cce | unit 39 run, 0 kept, 1 skipped | integration 39 run, 0 kept, 1 skipped | co…
2026-11-03 12:54:02 +01:00
jschoubben 43d5347258 An issue-groomer agent definition for playbook 03; .work/ is ignored, so worktrees never reach the public repository 2026-11-03 12:53:40 +01:00
mesh-tools b5347d0294 Merge pull request 'Agents work from hq's definitions and start from the knowledge base; every prepare and record gets the seams review' (#507) from docs/agents-from-hq into main 2026-11-03 11:48:55 +00:00
jschoubben 4315bb6307 A subagent works from hq; a coding one in a worktree under hq's .work, also reading its repository's conventions (the operator, 2026-11-03)
mesh/delivery merged: source.main moved to novox/hq@b5347d029453
mesh/check pass 4315bb6307c1 on f6bd4246b0d7 | carried from 979bf3f19cce | unit 39 run, 0 kept, 1 skipped | integration 39 run, 0 kept, 1 skipped | co…
2026-11-03 12:46:24 +01:00
jschoubben e8c154c3a0 Agent definitions: the developer on the smaller model unless the lead says why; review notes
mesh/delivery closed: a newer head of the same pull request
mesh/check pass e8c154c3a0ba on 9a2d398070ed | unit 39 run, 0 kept, 1 skipped | integration 39 run, 0 kept, 1 skipped | composition passed in 18s on 4…
2026-11-03 12:15:48 +01:00
jschoubben 666c9271dc mesh-developer: the test budget is read from the instructions, not copied 2026-11-03 12:15:19 +01:00
jschoubben d829fd6f9b Playbook 02: name no record in the seams rule 2026-11-03 12:14:48 +01:00
jschoubben 10df1c7be5 Playbooks overview: agents from hq's definitions; the seams review of every prepare 2026-11-03 12:14:25 +01:00
jschoubben 8420c6f236 Agents work from hq's definitions and start from the knowledge base; every prepare and record gets the seams review (the operator, 2026-11-03) 2026-11-03 12:14:12 +01:00
mesh-tools 9a2d398070 Merge pull request 'Bus state by template, no readers, and the ledger: designs 25, 32 and 55 and the glossary follow ADR 0595-0597' (#506) from docs/1966-bus-state into main 2026-11-03 10:21:09 +00:00
jschoubben 476557deeb The review record's seams row names the process's own tool subject
mesh/delivery merged: source.main moved to novox/hq@9a2d398070ed
mesh/check pass 476557deeb4b on f6bd4246b0d7 | carried from 979bf3f19cce | unit 39 run, 0 kept, 1 skipped | integration 39 run, 0 kept, 1 skipped | co…
2026-11-03 10:51:18 +01:00
jschoubben 3d4e361425 Designs 25, 32 and 55 record their review by architectural domain 2026-11-03 10:50:27 +01:00
jschoubben 0cd0cfd778 Design 25 names the signed caller and what the watchman newly hears; design 55 checks the database at every start before the drop 2026-11-03 10:49:43 +01:00
jschoubben c033592509 Design 25's bus module is unhealthy only on a verdict left unanswered; design 55 keeps its sent events in a state of its own 2026-11-03 10:49:10 +01:00
jschoubben 221165c712 Designs 25 and 55 answer the review by architectural domain: the bus module's events on its old sections, overdue creation raised by the watchman, the proven author, events and the database caught up after a restart, the bus module's integration test 2026-11-03 10:47:44 +01:00
jschoubben ed67d1f84f Bus state by template, no readers, and the ledger: designs 25, 32 and 55 and the glossary follow ADR 0595-0597
Research 082 graduated into three accepted records; the bus module's design (25) and the manifest's (32) are the home of bus state for every module, and the knowledge base (55) becomes its first ledger.
2026-11-03 10:42:58 +01:00
mesh-tools 08b968fd80 Merge pull request 'Design 47: the bus module is wanted like any module; only a warranted want replaces its server (ADR 0594)' (#505) from docs/774-bus-want into main 2026-11-03 01:09:31 +00:00
jschoubben b01f867345 Design 47: the bus module's want follows ADR 0594
mesh/delivery merged: source.main moved to novox/hq@08b968fd80cb
mesh/check pass b01f86734552 on f6bd4246b0d7 | carried from 979bf3f19cce | unit 39 run, 0 kept, 1 skipped | integration 39 run, 0 kept, 1 skipped | co…
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-03 02:02:39 +01:00
mesh-tools 92fa66f4e7 Merge pull request 'A research effort's reading tasks run on one agent definition, research-reader' (#504) from docs/research-reader into main 2026-11-03 00:51:58 +00:00
jschoubben 6bc1f7cc10 research-reader cites no path, as playbook 01 asks; AGENTS.md rewrapped
mesh/delivery merged: source.main moved to novox/hq@92fa66f4e79f
mesh/check pass 6bc1f7cc1040 on f6bd4246b0d7 | carried from 979bf3f19cce | unit 39 run, 0 kept, 1 skipped | integration 39 run, 0 kept, 1 skipped | co…
2026-11-03 01:45:29 +01:00
jschoubben 402ac3bfda Design 47: the assigner writes the bus module's want only to a node agent that keeps the server, and the test says so
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-03 01:44:38 +01:00
jschoubben ae4c90f990 Design 47: the bus module is wanted like any module, and only a put replaces its server (issue 774, the operator 2026-11-03)
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-03 01:37:31 +01:00
mesh-tools 4d4524cda3 Merge pull request 'Design 55: the knowledge base keeps its records on the bus and is written and reviewed by section (ADR 0592, ADR 0593 §10)' (#502) from feat/kb-nats-native into main 2026-11-03 00:07:23 +00:00
mesh-tools 5accd21320 Merge pull request 'Playbook 07 and design 47 after the controller left: run as the running checker, let the running parts judge, L2 delivered' (#503) from docs/1126-after-controller into main 2026-11-03 00:05:04 +00:00
jschoubben 3a3501d9f4 Playbook 07 and design 47: say how the two new rules are checked; glossary words; name where the bus module has no want
mesh/delivery merged: source.main moved to novox/hq@5accd2132013
mesh/check pass 3a3501d9f480 on f6bd4246b0d7 | carried from 979bf3f19cce | unit 39 run, 0 kept, 1 skipped | integration 39 run, 0 kept, 1 skipped | co…
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-03 00:58:19 +01:00
jschoubben 8402a7d773 Design 47: L2 is built and delivered as release sdk1, done once a check composes on every machine; L3 names what the controller's leaving left unwritten (issue 1126, issue 774)
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-03 00:57:29 +01:00
jschoubben 5d9a654767 Playbook 07: run every suite as the running checker places it, and let the running parts judge a release that changes them (issue 1126, release sdk1)
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-03 00:52:28 +01:00
jschoubben 1a389be090 A research effort's reading tasks run on one agent definition, research-reader, instead of a prompt written by hand each time 2026-11-03 00:28:06 +01:00
jschoubben c2b162a9e8 Design 55: readers by grant, the take-over refused while an Approve waits, the grants part's own test, §30 for nesting, the glossary's word in the review table
mesh/delivery merged: source.main moved to novox/hq@4d4524cda3e0
mesh/check pass c2b162a9e8da on f6bd4246b0d7 | carried from 979bf3f19cce | unit 39 run, 0 kept, 1 skipped | integration 39 run, 0 kept, 1 skipped | co…
2026-11-02 19:58:01 +01:00
jschoubben 3122e8be84 Design 55: follow ADR 0593: the mesh-sdk release sdk1 does the controller's part (the grants part creates, grants and warns; the indexer's judge checks the forms) 2026-11-02 19:55:40 +01:00
jschoubben 36517018d3 Design 55: one more use of the chosen word 2026-11-02 19:55:40 +01:00
jschoubben b8ad9da784 Design 55: the review section uses the word the operator chose, architectural domain 2026-11-02 19:55:40 +01:00
jschoubben eaa98758b9 Design 55: follow ADR 0592's tenth version: submit commits when its version is stored, and the tests for the rollback's double write and the Approve bound to its sections 2026-11-02 19:55:40 +01:00
jschoubben b9a42eaf90 Design 55: follow ADR 0592's eighth version: readers by ADR 0363's grant, extended to the object bucket; each cap within its form's bound; clause numbers after the splits 2026-11-02 19:55:40 +01:00
jschoubben 6256fc8e3b Design 55: follow ADR 0592's sixth version and the review by aspect: whole-or-refused writes, caps, the move's window and rollback, the delivery order, the judge's judged event, events with what they carry, a test per failure path, and a section Reviewed by aspect
The glossary gains opened and drafted, so a draft's first step is never called drafted.
2026-11-02 19:55:40 +01:00
jschoubben 012d276dcc Design 55: follow ADR 0592's revision: a stream that keeps its history, a module's own object bucket, the frame's bound and the move's full check 2026-11-02 19:55:40 +01:00
jschoubben 5a9a5d8d1d Design 55: the knowledge base keeps its records on the bus and is written and reviewed by section (ADR 0592), so a long record is judged whole and a draft lives only in the knowledge base
The glossary gains section, draft, form-only version and search index, and the homonyms section and draft.
2026-11-02 19:55:40 +01:00
mesh-tools 2731c796f0 Merge pull request 'hq's words test reads .claude/, so agent and skill definitions use the glossary's words' (#501) from docs/agent-roles-defined into main 2026-11-02 18:48:16 +00:00
jschoubben 7470927a07 Playbook 07 keeps to feature work: the rule on agent definitions goes to an instruction section for every session (the operator, 2026-11-02)
mesh/delivery merged: source.main moved to novox/hq@2731c796f0d6
mesh/check pass 7470927a07a0 on 2731c796f0d6 | carried from 7021d0cf5ac0 | unit 39 run, 0 kept, 1 skipped | integration 39 run, 0 kept, 1 skipped | co…
2026-11-02 19:45:17 +01:00
jschoubben 6c395cc206 Playbook 07: say how the agent-definition rule is checked
mesh/delivery closed: a newer head of the same pull request
mesh/check pass 6c395cc20667 on 7021d0cf5ac0 | unit 39 run, 0 kept, 1 skipped | integration 39 run, 0 kept, 1 skipped | composition passed
2026-11-02 19:42:08 +01:00
jschoubben 594a0871dd Playbook 07: a role started more than once is an agent definition in the repository it serves; hq's words test reads .claude/ 2026-11-02 19:41:34 +01:00
mesh-tools 7021d0cf5a Merge pull request 'Four reviewer agents carry playbook 01's review by architectural domain, two architectural domains each' (#500) from docs/domain-reviewers into main 2026-11-02 18:41:15 +00:00
jschoubben 4037bc1fb0 Reviewer agents: catalog, the glossary's word
mesh/delivery merged: source.main moved to novox/hq@7021d0cf5ac0
mesh/check pass 4037bc1fb0d7 on 2731c796f0d6 | carried from 7021d0cf5ac0 | unit 39 run, 0 kept, 1 skipped | integration 39 run, 0 kept, 1 skipped | co…
2026-11-02 19:39:19 +01:00
jschoubben 121b582ebc Four reviewer agents carry playbook 01's review by architectural domain, two architectural domains each, so every review asks the same questions 2026-11-02 19:38:35 +01:00
mesh-tools fa3d8aaeec Merge pull request 'Playbooks 01 and 02: review by architectural domain, sections named by title with no file references, and storage forms that fit (the operator, 2026-11-02)' (#499) from docs/review-by-aspect into main 2026-11-02 18:27:14 +00:00
jschoubben 633c587a1e Glossary: bare domain keeps the mesh-domain sense; only the review's sense is always qualified, so the homonym rule holds without rewriting every use
mesh/delivery merged: source.main moved to novox/hq@fa3d8aaeec69
mesh/check pass 633c587a1eef on 2731c796f0d6 | carried from 7021d0cf5ac0 | unit 39 run, 0 kept, 1 skipped | integration 39 run, 0 kept, 1 skipped | co…
2026-11-02 19:22:45 +01:00
jschoubben 6f2bacf6c4 Architectural domain for the review's eight concerns, and the glossary's domain qualified as mesh domain in the homonyms, as the operator chose on 2026-11-02 2026-11-02 19:21:48 +01:00
jschoubben f5624e7d9f Review fixes: the research skill names sections by title, the drawn overview's order and bounds follow the playbooks, an amended design is reviewed by aspect, and storage forms use glossary words 2026-11-02 18:20:12 +01:00
jschoubben b861b3ae36 Glossary: review aspect; playbook 01: a piece of state takes the bus storage form that fits it, and a form added to mesh-sdk is named to the operator with its cost 2026-11-02 18:19:05 +01:00
jschoubben 0ac621e03f Playbooks 01 and 02: review a direction and a design by aspect before proposing or merging, and name a record's sections by title with no file references, as the operator asked on 2026-11-02 2026-11-02 18:19:05 +01:00
mesh-tools f312e7cd0e Merge pull request 'Playbook 07: the prepare step reads what exists today before any code' (#498) from docs/pb07-what-exists into main 2026-11-02 15:16:21 +00:00
jschoubben 2b9e6b256b Read what exists before writing code, in the prepare step
mesh/delivery merged: source.main moved to novox/hq@f312e7cd0ea6
mesh/check pass 2b9e6b256b4b on f312e7cd0ea6 | carried from 1747485cb351 | unit 39 run, 0 kept, 1 skipped | integration 39 run, 0 kept, 1 skipped | co…
Each layer (research, decisions, survey, review) checked the layer before
it and none read the running code, so effects of the controller's removed
jobs surfaced only after delivery. Playbook 07's prepare step now writes,
per context file, what exists today, every effect and reader of what is
removed or moved, and each one's fate with its test; the review refuses
work without it. Playbook 08 reads its area the same way, briefly.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 16:14:17 +01:00
mesh-tools 1747485cb3 Merge pull request 'Design 47 and the glossary follow what L3 to L7 built (issues 774, 1952, 1953, 1954, 1955)' (#496) from feat/774-l3-reland into main 2026-10-30 11:01:45 +00:00
jschoubben 0c450cad25 Design 47: the L4 row cites ADR 0572, the self-check probes kept and dropped
mesh/delivery merged: source.main moved to novox/hq@1747485cb351
mesh/delivery-group group feat/774-l3-reland merged: every member is merged
mesh/check pass 0c450cad258e on 1747485cb351 | unit 39 run, 0 kept, 1 skipped | integration 39 run, 0 kept, 1 skipped | composition passed in 8.3s
2026-10-30 01:16:33 +01:00
jschoubben 2e235286b1 Glossary: cite ADR 0571's accepted insight for the artifact sweep's seat name 2026-10-30 00:03:39 +01:00
jschoubben a35c7ed6d0 Glossary and design 47: the release's new words and the L3-L7 homes as decided (ADR 0563-0571), so readers find where each controller job now lives 2026-10-29 23:58:11 +01:00
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 1747485cb351 | carried from e65f897a1fda | unit 39 run, 0 kept, 1 skipped | integration 39 run, 0 kept, 1 skipped | co…
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 delivered
mesh/delivery-group group feat/r6 merged: every member is merged
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 delivered
mesh/delivery-group group feat/r4g-finish merged: every member is merged
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
mesh-tools 022392a22b Merge pull request 'Design 47: release 3 is the milestones R3a to R3d by ADR 0437, and answering asks in the view moves to R5' (#429) from docs/design-47-r3-cut into main 2026-10-19 08:22:06 +00:00
jschoubben 643ec2b1bf Design 47: release 3 is the milestones R3a to R3d by ADR 0437, and answering asks in the view moves to R5
mesh/merge-gate head 643ec2b1bf3a merged into trunk ee862d8e2260 — pass: the change touches no module of the mesh's graph
mesh/repo-check head 643ec2b1bf3a merged into trunk ee862d8e2260 — pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-19 10:19:55 +02:00
mesh-tools ee862d8e22 Merge pull request 'Design 47: R2c is split by its order, the controller's last heard from presence before the node-engine stops sending alive (issue 1183)' (#428) from docs/design-47-r2c-split into main 2026-10-19 00:23:27 +00:00
jschoubben 292fae8863 Design 47: R2c is split by its order, the controller's last heard from presence before the node-engine stops sending alive (issue 1183)
mesh/delivery delivered
mesh/merge-gate out of date: judged against trunk f9139feb30c2, the trunk is now ee862d8e2260; judged again on a recheck or when it is merged
mesh/repo-check out of date: judged against trunk f9139feb30c2, the trunk is now ee862d8e2260; judged again on a recheck or when it is merged
2026-10-19 02:12:16 +02:00
mesh-tools f9139feb30 Merge pull request 'Design 47: release 1-2 is the six milestones R1a to R2c, each one release; playbook 04 splits a milestone that holds a release order' (#427) from docs/design-47-milestones-r1a-r2c into main 2026-10-18 22:17:17 +00:00
jschoubben d10965a341 Design 47: release 1-2 is the six milestones R1a to R2c, each one release; playbook 04 splits a milestone that holds a release order (the operator, 2026-10-19)
mesh/merge-gate head d10965a34148 merged into trunk f7915f7f2952 — pass: the change touches no module of the mesh's graph
mesh/repo-check head d10965a34148 merged into trunk f7915f7f2952 — pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-19 00:11:02 +02:00
mesh-tools f7915f7f29 Merge pull request 'Playbook 07: the prepare step looks for the orders a release hides, and fixtures come from the writer's shape' (#426) from docs/prepare-checks-release-orders into main 2026-10-18 19:33:32 +00:00
jschoubben 86e66dd51f Playbook 07: the prepare step looks for the orders a release hides, and fixtures come from the writer's shape (design 47 release 1-2, 2026-10-18)
mesh/merge-gate head 86e66dd51fcf merged into trunk a353ed21c0a1 — pass: the change touches no module of the mesh's graph
mesh/repo-check head 86e66dd51fcf merged into trunk a353ed21c0a1 — pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-18 21:26:06 +02:00
mesh-tools a353ed21c0 Merge pull request 'Design 47: release 1-2 cut by the orders its prepare step found, with its new decisions and issues' (#425) from docs/design-47-release-1-2-cuts into main 2026-10-18 18:36:19 +00:00
jschoubben 4a090df25a Design 47: release 1-2 cut by the orders its prepare step found, with its new decisions and issues (issue 1130)
mesh/merge-gate head 4a090df25a6a merged into trunk ea7c831196b6 — pass: the change touches no module of the mesh's graph
mesh/repo-check head 4a090df25a6a merged into trunk ea7c831196b6 — pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-18 20:31:36 +02:00
mesh-tools ea7c831196 Merge pull request 'Design 47: the operator confirmed ADR 0429's releases as milestones; release 1-2's issues and their order' (#424) from docs/design-47-delivery-releases into main 2026-10-18 15:57:42 +00:00
jschoubben d81c23b65a Design 47: the operator confirmed the releases as milestones (2026-10-18); release 1-2's issues and their order
mesh/merge-gate head d81c23b65a55 merged into trunk 0f8d778ec25b — pass: the change touches no module of the mesh's graph
mesh/repo-check head d81c23b65a55 merged into trunk 0f8d778ec25b — pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-18 17:55:21 +02:00
mesh-tools 0f8d778ec2 Merge pull request 'Design 47: the six releases of ADR 0429 as milestones; glossary: the delivery domain, its parts and their words' (#423) from docs/design-47-delivery-releases into main 2026-10-18 15:50:42 +00:00
jschoubben cd9336d05a Design 47 and glossary: review round 1 (the sections stay the design until their release, rule 4's step above zero, the Approve on the phone until issue 953, release of a milestone under The record)
mesh/merge-gate head cd9336d05aca merged into trunk ba127e29b796 — pass: the change touches no module of the mesh's graph
mesh/repo-check head cd9336d05aca merged into trunk ba127e29b796 — pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-18 17:48:15 +02:00
jschoubben 5c81f1170c Design 47: the six releases of ADR 0429 as milestones, milestone 6 folded into release 4; glossary: the delivery domain, its parts and their words (ADR 0428, ADR 0429)
Research 063 graduated into ADR 0429 and this design (playbook 02 step 3).
The design names the parts and the releases; its other sections hold until
each release is delivered and are rewritten as each is prepared (issue 1130).
The words the plans use are marked as going at release 6, not struck: they
stay true until that release is delivered.
2026-10-18 17:45:59 +02:00
mesh-tools ba127e29b7 Merge pull request 'Playbook 02: after three review rounds the lead fixes further findings itself; only a finding that changes what was decided goes to the operator' (#422) from docs/review-rounds-lead-fixes into main 2026-10-18 15:09:15 +00:00
jschoubben d0a2950bb0 Playbook 02: wrap the check line and name the step it means (review round 1)
mesh/merge-gate head d0a2950bb0c0 merged into trunk 42ecaa7ac419 — pass: the change touches no module of the mesh's graph
mesh/repo-check head d0a2950bb0c0 merged into trunk 42ecaa7ac419 — pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-18 17:05:46 +02:00
jschoubben 8e76098412 Playbook 02: after three review rounds the lead fixes further findings itself and proposes again; only a finding that changes what was decided goes to the operator (the operator, 2026-10-18) 2026-10-18 17:04:38 +02:00
mesh-tools 42ecaa7ac4 Merge pull request 'Playbooks 07 and 08: integration tests run once through mesh-controller.try before the pull request, never on the machine' (#421) from docs/integration-tests-through-try into main 2026-10-18 13:05:03 +00:00
jschoubben cec72d1dc9 Playbooks 07 and 08: integration tests run once through mesh-controller.try on the pushed branch before the pull request, never on the machine (the operator, 2026-10-18)
mesh/delivery delivered
mesh/merge-gate head cec72d1dc962 merged into trunk b98b93efc8b0 — pass: the change touches no module of the mesh's graph
mesh/repo-check head cec72d1dc962 merged into trunk b98b93efc8b0 — pass: its merge-check.sh passed
2026-10-18 15:02:56 +02:00
mesh-tools b98b93efc8 Merge pull request 'Playbooks 07 and 08: tests are written from acceptance criteria on the issue (issue 1118); design 47 gets milestone 5b and cites ADR 0426 (issue 1117)' (#420) from docs/acceptance-criteria-milestone-5b into main 2026-10-18 10:52:52 +00:00
jschoubben ec373556d0 Playbooks 07 and 08: an acceptance criterion is numbered and cited by its test as <issue>/AC<n>, so its check is a grep (review round 1)
mesh/merge-gate head ec373556d0bb merged into trunk 672011ab92ad — pass: the change touches no module of the mesh's graph
mesh/repo-check head ec373556d0bb merged into trunk 672011ab92ad — pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-18 12:50:30 +02:00
jschoubben f242b8763b Design 47 builds with ADR 0426 (no timer where an event can end the wait) 2026-10-18 12:49:18 +02:00
jschoubben bb6af05f5c Playbooks 07 and 08: tests are written from acceptance criteria the larger model writes onto the issue (issue 1118); design 47 gets milestone 5b, the planning out of the controller (issue 1117) 2026-10-18 12:42:06 +02:00
mesh-tools 672011ab92 Merge pull request 'Design 55: designs and the glossary become records, and the knowledge base owns a design's state (ADR 0423, ADR 0424)' (#419) from design/055-designs-and-glossary-as-records into main 2026-10-18 02:28:14 +00:00
jochen 05ff547358 Design 55: hq owns part of the third slice's code, its checks and links (review round 1)
mesh/delivery delivered
mesh/merge-gate head 05ff547358fe merged into trunk 76a671b8c5da — pass: the change touches no module of the mesh's graph
mesh/repo-check head 05ff547358fe merged into trunk 76a671b8c5da — pass: its merge-check.sh passed
2026-10-18 04:25:11 +02:00
jochen 1def984c07 Design 55: designs and the glossary become records, and the knowledge base owns a design's state (ADR 0423, ADR 0424) 2026-10-18 04:23:10 +02:00
mesh-tools 76a671b8c5 Merge pull request 'Retire the word epic, and define milestone as a group of a design's issues delivered together (issue 1105)' (#418) from fix/1105-retire-epic into main 2026-10-18 02:10:49 +00:00
jschoubben 19bd7fd960 Retire the word epic, and define milestone as a group of a design's issues delivered together (issue 1105)
mesh/merge-gate head 19bd7fd96066 merged into trunk f82a90dfa021 — pass: the change touches no module of the mesh's graph
mesh/repo-check head 19bd7fd96066 merged into trunk f82a90dfa021 — pass: its merge-check.sh passed
mesh/delivery delivered
The operator decided on 2026-10-18 that a domain word is used only when clearly defined. epic had three uses and no code; design 47 now names two ways, hotfix and feature.
2026-10-18 04:08:15 +02:00
mesh-tools f82a90dfa0 Merge pull request 'Playbook 07: a new integration test enters through the path the live trigger takes (issue 1086)' (#417) from fix/1086-test-enters-live-path into main 2026-10-18 00:23:21 +00:00
jschoubben 17c27ddf83 Playbook 07: a new integration test enters through the path the live trigger takes (issue 1086)
mesh/merge-gate head 17c27ddf834f merged into trunk 5c6b645c6ba9 — pass: the change touches no module of the mesh's graph
mesh/repo-check head 17c27ddf834f merged into trunk 5c6b645c6ba9 — pass: its merge-check.sh passed
mesh/delivery delivered
Milestone 4's test saved its plan by hand, skipped the cut from a batch, and passed while the backup step never ran.
2026-10-18 02:19:20 +02:00
mesh-tools 5c6b645c6b Merge pull request 'Design 47: milestone 3 fix names issue 1079 (1084 was its duplicate)' (#416) from fix/design-47-milestone-row into main 2026-10-17 23:51:00 +00:00
jschoubben b6cf49d8ce Design 47: milestone 3 fix names issue 1079, of which 1084 was closed as a duplicate, and the forge module it touched
mesh/merge-gate head b6cf49d8ce12 merged into trunk 1484ff76826c — pass: the change touches no module of the mesh's graph
mesh/repo-check head b6cf49d8ce12 merged into trunk 1484ff76826c — pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-18 01:49:37 +02:00
mesh-tools 1484ff7682 Merge pull request 'Design 47: milestone 4 delivered; a fix of milestone 3 comes next. Playbook 07: new integration tests are read against the code before the first push (issues 980, 1082)' (#415) from feat/milestone-4-delivered into main 2026-10-17 22:32:35 +00:00
jschoubben c10efa4d8c Playbook 07: the new test failed on its first two runs, not three
mesh/merge-gate head c10efa4d8c8f merged into trunk be5284f7b86c — pass: the change touches no module of the mesh's graph
mesh/repo-check head c10efa4d8c8f merged into trunk be5284f7b86c — pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-18 00:30:41 +02:00
jschoubben 24fd567c31 Design 47: milestone 4 delivered and split; a fix of milestone 3 comes next. Playbook 07: new integration tests are read against the code before the first push (issues 980, 1082) 2026-10-18 00:29:35 +02:00
mesh-tools be5284f7b8 Merge pull request 'Say where nine half-built designs stand (issue 1072)' (#414) from fix/1072-design-states into main 2026-10-17 21:19:27 +00:00
jochen 21c4d212ec Correct three facts in the designs' states (issue 1072, review round 1)
mesh/merge-gate head 21c4d212ec8d merged into trunk 9d59c2ce1453 — pass: the change touches no module of the mesh's graph
mesh/repo-check head 21c4d212ec8d merged into trunk 9d59c2ce1453 — pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-17 22:55:02 +02:00
jochen 1bd6f4611e Say where nine half-built designs stand (issue 1072)
The records cleanup found them saying proposed or designed although their
code is on main; each now says in-progress and what is built and left.
2026-10-17 22:53:10 +02:00
mesh-tools 9d59c2ce14 Merge pull request 'Playbooks 07 and 08 without the ready mark; a fixing agent runs no integration test; large issues split; the merge script ships first (issues 1022, 895)' (#413) from feat/one-group-one-plan into main 2026-10-17 20:10:56 +00:00
jschoubben 64e357da7d Playbook 07: a change the new merge script must judge ships the script first (issue 895)
mesh/merge-gate head 64e357da7d7e merged into trunk a6db6a70cffa beside mesh-catalog#352@07717974 +1 more — pass: the change touches no module of the mesh'…
mesh/repo-check head 64e357da7d7e merged into trunk a6db6a70cffa beside mesh-catalog#352@07717974 +1 more — pass: its merge-check.sh passed
mesh/delivery delivered
mesh/delivery-group group feat/one-group-one-plan merged: every member is merged
2026-10-17 21:32:35 +02:00
jschoubben 9a9a78f882 Playbook 07: report every ten minutes (the operator, 2026-10-17) 2026-10-17 21:28:46 +02:00
jschoubben e816b61cb7 Playbook 07: the prepare step splits an issue sized over an hour (the operator, 2026-10-17) 2026-10-17 21:25:39 +02:00
jschoubben 610be93803 Playbook 07: a fixing agent never runs an integration test locally (the operator, 2026-10-17) 2026-10-17 21:23:05 +02:00
jschoubben d5ddd1e715 Playbooks 07 and 08: no ready mark; the review record rides in the pull request and the Approve is asked once the checks pass (issue 1022, ADR 0421) 2026-10-17 20:55:47 +02:00
mesh-tools a6db6a70cf Merge pull request 'Playbook 07: a milestone has steps, runs the whole unit suite, writes its findings into its issues, watches on the smaller model; design 47 names milestone 2b and takes 895 (issue 1053)' (#412) from fix/1053-steps-not-phases into main 2026-10-17 18:29:24 +00:00
mesh-tools e44ec3f612 Merge pull request 'Playbook 07: a new node-engine key needs the controller first, on a branch of its own (issue 1051)' (#411) from fix/1051-pb07-controller-first into main 2026-10-17 18:28:25 +00:00
mesh-tools dbc7406490 Playbook 07: a new node-engine key needs the controller's pin moved first, and a release that must merge first has a branch name of its own (issue 1051)
mesh/merge-gate head dbc740649039 merged into trunk 90c67930969b — pass: the change touches no module of the mesh's graph
mesh/repo-check head dbc740649039 merged into trunk 90c67930969b — pass: its merge-check.sh passed
mesh/delivery delivered
Feature owned-paths (issue 1024) planned no controller release for a new key on directory resources, though the controller judges every machine with the node-engine's vendored validator; and the controller's release, on the feature's branch name, joined the catalogue's delivery group and could not merge before it.
2026-10-17 18:19:35 +00:00
jschoubben 2001d6e063 Playbook 07: a milestone has steps, runs the whole unit suite, writes its findings into its issues and watches on the smaller model; design 47 names milestone 2b and takes 895 into milestone 3 (issue 1053)
mesh/merge-gate head 2001d6e06334 merged into trunk e44ec3f61235 — pass: the change touches no module of the mesh's graph
mesh/repo-check head 2001d6e06334 merged into trunk e44ec3f61235 — pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-17 20:19:16 +02:00
mesh-tools 90c6793096 Merge pull request 'Design 29: a folder a module owns says how it treats files it did not write (issue 1024)' (#409) from feat/owned-paths into main 2026-10-17 18:14:10 +00:00
mesh-tools 499407c669 Take in main up to 2cf1c64e24 (issue 1024)
mesh/merge-gate head 499407c66936 merged into trunk 2cf1c64e24f5 beside mesh-catalog#349@7104df6f — pass: the change touches no module of the mesh's graph
mesh/repo-check head 499407c66936 merged into trunk 2cf1c64e24f5 beside mesh-catalog#349@7104df6f — pass: its merge-check.sh passed
mesh/delivery delivered
mesh/delivery-group group feat/owned-paths merged: every member is merged
hq main moved after this change's check; it touches neither design 29 nor anything this change edits. The branch's tree is the reviewed design 29 change merged with main.
2026-10-17 18:09:09 +00:00
mesh-tools 2cf1c64e24 Merge pull request 'Playbook 07: the reviewer is kept for the whole release, and a watcher waits for its checks' (#410) from fix/pb07-reviewer-kept into main 2026-10-17 16:34:55 +00:00
jschoubben e1987b3654 Playbook 07: the reviewer is kept for the whole release, and a watcher waits for its checks (issue 1042)
mesh/merge-gate head e1987b365427 merged into trunk c87ab9af0f9a — pass: the change touches no module of the mesh's graph
mesh/repo-check head e1987b365427 merged into trunk c87ab9af0f9a — pass: its merge-check.sh passed
mesh/delivery delivered
The operator asked why review takes so long: a fresh reviewer per round or per mark reads
everything again, and a reviewer waiting on the forge's checks spends its run waiting.
2026-10-17 18:26:06 +02:00
mesh-tools c87ab9af0f Merge pull request 'Playbook 07: a design names its milestones' issues, never their delivery state' (#408) from fix/pb07-no-state-in-designs into main 2026-10-17 16:23:52 +00:00
mesh-tools 4e0dd1b0d3 Say in design 29 how a folder a module owns treats files it did not write (issue 1024, ADR 0422)
mesh/merge-gate out of date: judged against trunk 7a1d3d89f0b9, the trunk is now c87ab9af0f9a; judged again on a recheck or when it is merged
mesh/repo-check out of date: judged against trunk 7a1d3d89f0b9, the trunk is now c87ab9af0f9a; judged again on a recheck or when it is merged
mesh/delivery-group group feat/owned-paths checking: 0 of 2 member(s) ready
mesh/delivery superseded: a newer head of the same pull request
Design 29 is ADR 0422's home: it now says what `unknown` and `knows` mean on a directory under a home, that the found class no longer holds in ~/.ssh (ADR 0415, 0417), and lists ADR 0412, 0415, 0416, 0417 and 0422 among its decisions.
2026-10-17 16:23:05 +00:00
jschoubben b5d3f23947 Design 57: its milestones name their issues, not their delivery state (issue 1039, folded into 1038)
mesh/merge-gate head b5d3f2394798 merged into trunk 7a1d3d89f0b9 — pass: the change touches no module of the mesh's graph
mesh/repo-check head b5d3f2394798 merged into trunk 7a1d3d89f0b9 — pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-17 18:20:58 +02:00
jschoubben c753ff068a Playbook 07: a design names its milestones' issues, never their delivery state (issue 1038)
mesh/merge-gate head c753ff068a55 merged into trunk 7a1d3d89f0b9 — pass: the change touches no module of the mesh's graph
mesh/repo-check head c753ff068a55 merged into trunk 7a1d3d89f0b9 — pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
The tracker and the delivery records hold whether a milestone is delivered, when and by what;
a copy in the design's table is a central status kept by hand (AGENTS.md). Design 47 loses its
copied cells.
2026-10-17 18:18:45 +02:00
mesh-tools 7a1d3d89f0 Merge pull request 'Design 47: phase II release 1 delivered (issue 1025)' (#407) from fix/design-47-phase-2-release-1-delivered into main 2026-10-17 16:11:22 +00:00
mesh-tools cb79a28a80 Merge pull request 'Playbook 07: every wait on the wait list has a watcher agent on the bus' (#406) from fix/pb07-read-the-whole-log into main 2026-10-17 16:09:28 +00:00
jschoubben 0b9fda46cc Design 47: phase II release 1 delivered, 2026-10-17 (playbook 07, write-back)
mesh/merge-gate head 0b9fda46cc72 merged into trunk cb79a28a8062 — pass: the change touches no module of the mesh's graph
mesh/repo-check head 0b9fda46cc72 merged into trunk cb79a28a8062 — pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-17 18:04:17 +02:00
jschoubben 6b58a6af82 Playbook 07: the glossary's word, the builder (review of #405)
mesh/merge-gate head 6b58a6af8246 merged into trunk c0b1c47be6b4 — pass: the change touches no module of the mesh's graph
mesh/repo-check head 6b58a6af8246 merged into trunk c0b1c47be6b4 — pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-17 18:03:52 +02:00
jschoubben 9a39785116 Playbook 07: every wait on the wait list has a watcher agent on the bus (the operator, 2026-10-17)
mesh/merge-gate waiting in the build queue since 18:01
2026-10-17 18:00:35 +02:00
mesh-tools c0b1c47be6 Merge pull request 'Playbook 07: read a failed check's whole log; a change to merge-check.sh is judged by main's version' (#405) from fix/pb07-read-the-whole-log into main 2026-10-17 15:58:25 +00:00
jschoubben 06440c1522 Playbook 07: the build seat's holder, not a retired word (review of #405's check)
mesh/merge-gate head 06440c152298 merged into trunk cf73f9679771 — pass: the change touches no module of the mesh's graph
mesh/repo-check head 06440c152298 merged into trunk cf73f9679771 — pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-17 17:56:21 +02:00
jschoubben 0e7eb9c2da Playbook 07: read a failed check's whole log; a change to merge-check.sh is judged by main's version
mesh/merge-gate head 0e7eb9c2dac8 merged into trunk cf73f9679771 — pass: the change touches no module of the mesh's graph
mesh/repo-check head 0e7eb9c2dac8 merged into trunk cf73f9679771 — fail: its merge-check.sh failed: words: 1 problem(s)
mesh/delivery superseded: a newer head of the same pull request
2026-10-17 17:29:43 +02:00
mesh-tools cf73f96797 Merge pull request 'Design 47: one plan per group, no order lines, the Approve as the only gate (ADR 0418, 0419, 0421)' (#404) from fix/design-47-records-0418-0421 into main 2026-10-17 15:25:14 +00:00
jschoubben b0cfd08f7c Design 47: ADR 0420 accepted, ADR 0249 superseded, milestone 3's issues named (review of #404)
mesh/merge-gate head b0cfd08f7c97 merged into trunk aaa0647ec31c — pass: the change touches no module of the mesh's graph
mesh/repo-check head b0cfd08f7c97 merged into trunk aaa0647ec31c — pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-17 17:20:16 +02:00
jschoubben 0a6613f0d4 Design 47: one plan per group, no order lines, the Approve as the only gate (ADR 0418, 0419, 0421)
mesh/merge-gate head 0a6613f0d4b8 merged into trunk aaa0647ec31c — pass: the change touches no module of the mesh's graph
mesh/repo-check head 0a6613f0d4b8 merged into trunk aaa0647ec31c — pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
2026-10-17 17:18:03 +02:00
mesh-tools aaa0647ec3 Merge pull request 'Design 47: milestones 1 and 2 delivered' (#403) from fix/design-47-milestone-2-delivered into main 2026-10-17 13:10:39 +00:00
mesh-tools de32fb82ea Merge pull request 'Playbook 07: the full report shows the whole design's milestones' (#402) from fix/pb07-report-shows-the-design into main 2026-10-17 13:08:59 +00:00
jschoubben 85a729c607 Design 47: milestones 1 and 2 delivered, 2026-10-17 (playbook 07, write-back)
mesh/merge-gate head 85a729c607a1 merged into trunk de32fb82ea73 — pass: the change touches no module of the mesh's graph
mesh/repo-check head 85a729c607a1 merged into trunk de32fb82ea73 — pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-17 15:06:38 +02:00
jschoubben fb284fa6ee Playbook 07: the full report shows the whole design's milestones, the running one's issues under it (the operator, 2026-10-17)
mesh/merge-gate head fb284fa6ee2a merged into trunk 2b04a5f0a1aa — pass: the change touches no module of the mesh's graph
mesh/repo-check head fb284fa6ee2a merged into trunk 2b04a5f0a1aa — pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-17 14:13:28 +02:00
mesh-tools 2b04a5f0a1 Merge pull request 'Playbooks 07 and 04: a milestone is one release, never cut for size or time' (#401) from fix/pb07-a-milestone-is-one-release into main 2026-10-17 11:50:34 +00:00
jschoubben addbd74c39 Playbook 04 and design 47: the milestone is one release, not a feature step (review of #401)
mesh/merge-gate head addbd74c3924 merged into trunk 0f63a73def93 — pass: the change touches no module of the mesh's graph
mesh/repo-check head addbd74c3924 merged into trunk 0f63a73def93 — pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-17 13:47:37 +02:00
jschoubben 6bf18062c4 Playbooks 07 and 04: a milestone is one release, never cut for size or time; milestones are cut by release order (the operator, 2026-10-17) 2026-10-17 13:47:21 +02:00
mesh-tools 0f63a73def Merge pull request 'Describe the mesh MCP server as it runs and mark its design implemented (issue 1008)' (#400) from fix/1008-mcp-server-shipped into main 2026-10-17 11:43:03 +00:00
mesh-tools d0ee14712e Merge pull request 'Playbooks 07 and 08: a repository's own clone stays on main (issue 1010)' (#399) from fix/1010-clone-stays-on-main into main 2026-10-17 11:40:24 +00:00
jochen f6199fafce As-is 13: fix five review findings on the mesh MCP server (issue 1008, review round 1)
mesh/merge-gate head f6199fafcee4 merged into trunk d0ee14712e18 — pass: the change touches no module of the mesh's graph
mesh/repo-check head f6199fafcee4 merged into trunk d0ee14712e18 — pass: its merge-check.sh passed
mesh/delivery delivered
node-tools still has a manifest (invokes *, waits, the mcp-endpoint provision) —
describe it in prose instead of saying it has none. The TypeScript sources
remain on main; only the built artifact is Go. The mesh-controller seat
answers 63 verbs as its own runtime, not 60-93 per node (that is node-tools'
own node-scoped seat verbs). Prefer reading mesh_overview live over a count
that drifts. An as-is document amends nothing — reworded accordingly.
2026-10-17 13:39:14 +02:00
mesh-tools d901156614 Merge pull request 'Playbook 07: the lead writes a design's state back (issue 1009)' (#398) from fix/design-status-written-back into main 2026-10-17 11:38:12 +00:00
jochen e934a0286f Playbook 08: check that the clone stays on main (issue 1010, review round 1)
mesh/merge-gate head e934a0286f7f merged into trunk d9011566146e — pass: the change touches no module of the mesh's graph
mesh/repo-check head e934a0286f7f merged into trunk d9011566146e — pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-17 13:37:09 +02:00
jochen 8dbfd8d8ff Playbooks 07 and 08: a repository's own clone stays on main (issue 1010)
A clone left on an old work branch hands the next session stale playbooks
and designs as if current.
2026-10-17 13:36:37 +02:00
jochen 2cea6c9b0c Date design 57's delivered milestones (issue 1009, review round 2)
mesh/merge-gate head 2cea6c9b0cf4 merged into trunk 84b464d109c0 — pass: the change touches no module of the mesh's graph
mesh/repo-check head 2cea6c9b0cf4 merged into trunk 84b464d109c0 — pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-17 13:35:56 +02:00
jochen 79dcc0a6ec Give design 57 its milestones and check every write-back moment (issue 1009, review round 1) 2026-10-17 13:35:20 +02:00
jochen 65f69d96a9 Describe the mesh MCP server as it runs and mark its design implemented (issue 1008)
mesh-console is gone: the tool runner (node-tools) is a core component on
every node, host-side and never a container (ADR 0175), announcing seven
tools by address (ADR 0195, 0367), not the five-tool container the old
as-is doc described. The to-be document's unsettled items get owners:
the mesh's own verbs already shipped (ADR 0154); identity and
per-assignment grants are design 58 milestone 6 (issue 883).
2026-10-17 13:35:12 +02:00
jochen b83a1e748e Playbook 07: the lead writes a design's state back (issue 1009)
Design 57 was merged as a release with three tasks left and still said
designed, so another session took it for unbuilt. Say it in-progress, with
what is left and when it is built.
2026-10-17 13:33:37 +02:00
mesh-tools 84b464d109 Merge pull request 'Playbook 07: one design is built at a time (issue 1007)' (#397) from fix/pb07-one-design-at-a-time into main 2026-10-17 11:26:12 +00:00
jochen 88d7db36c8 Playbook 07: the check counts milestone issues only (issue 1007, review round 1)
mesh/merge-gate head 88d7db36c866 merged into trunk c61f01d63eb8 — pass: the change touches no module of the mesh's graph
mesh/repo-check head 88d7db36c866 merged into trunk c61f01d63eb8 — pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-17 13:23:21 +02:00
jochen 974b3e7d42 Playbook 07: one design is built at a time (issue 1007)
The operator decided on 2026-10-17 that a second design's milestones wait
until the design being built is delivered; nothing in the playbooks said so.
2026-10-17 13:22:41 +02:00
mesh-tools c61f01d63e Merge pull request 'Playbook 07: one milestone is one branch per repository' (#395) from fix/pb07-one-branch-per-milestone into main 2026-10-17 10:20:34 +00:00
mesh-tools 4325467b4d Merge pull request 'Design 58: milestones 11 to 14 from research 057, as-is 17, and role names in place of machine and login names' (#396) from design/058-milestones-11-to-14 into main 2026-10-17 10:19:33 +00:00
mesh-tools 0a010b8b79 Design 58: milestone 13 does not wait for milestone 7; until then nox's dangerous acts are refused (review round 1 of #396)
mesh/merge-gate head 0a010b8b79a1 merged into trunk 85f0aecf5ad6 — pass: the change touches no module of the mesh's graph
mesh/repo-check head 0a010b8b79a1 merged into trunk 85f0aecf5ad6 — pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-17 10:17:00 +00:00
mesh-tools cc0503ef66 Design 58 and 36: role names for the machines and no operator logins, as a public repository needs (fixes what #394 wrote)
mesh/merge-gate head cc0503ef663a merged into trunk 85f0aecf5ad6 — pass: the change touches no module of the mesh's graph
mesh/repo-check head cc0503ef663a merged into trunk 85f0aecf5ad6 — pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
2026-10-17 10:13:02 +00:00
mesh-tools 56d8d546c7 Design 58: milestones 11 to 14 from research 057 with their issues, and as-is 17 on today's accounts (playbook 04) 2026-10-17 10:10:42 +00:00
jschoubben edff1f25a1 Playbook 07: one milestone is one branch per repository, its steps reuse the name (the operator, 2026-10-17)
mesh/merge-gate head edff1f25a10b merged into trunk 4325467b4d59 — pass: the change touches no module of the mesh's graph
mesh/repo-check head edff1f25a10b merged into trunk 4325467b4d59 — pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-17 12:04:37 +02:00
mesh-tools 85f0aecf5a Merge pull request 'Design 58 and 36: identities carry logins, run for an identity, a bus password per identity (research 057)' (#394) from design/057-identities into main 2026-10-17 09:54:42 +00:00
mesh-tools 0dc8cab178 Design 58 and 36: cite ADR 0266's ways to root, and make acceptance tests 7 and 9 and the nox glossary entry agree with ADR 0411, 0412 and 0414 (review round 1)
mesh/merge-gate head 0dc8cab1781b merged into trunk 375e77690b8a — pass: the change touches no module of the mesh's graph
mesh/repo-check head 0dc8cab1781b merged into trunk 375e77690b8a — pass: its merge-check.sh passed
mesh/delivery delivered
2026-10-17 09:40:48 +00:00
mesh-tools 98b731d5d1 Design 58 and 36: identities carry logins, run for an identity, a bus password per identity (ADR 0411 to 0414, research 057)
mesh/merge-gate head 98b731d5d1ff merged into trunk 375e77690b8a — pass: the change touches no module of the mesh's graph
mesh/repo-check head 98b731d5d1ff merged into trunk 375e77690b8a — pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
2026-10-17 09:35:37 +00:00
337 changed files with 20645 additions and 2553 deletions
+47
View File
@@ -0,0 +1,47 @@
---
name: hq-design-writer
description: Writes one set of amendments to hq's to-be designs, glossary and playbooks from accepted decisions and the operator's answers to design questions (hq playbook 02, amending a design and answering a design question; playbook 04 step 4a), in the shape a builder reads whole - a main file and one milestone part per milestone, numbered design lines that leave the builder no choice - in a worktree on a branch of its own, ready for the review by architectural domain. Writes only in its worktree; never pushes.
tools: Read, Grep, Glob, Bash, Write, Edit, mcp__mesh__mesh_call, mcp__mesh__mesh_describe, mcp__mesh__mesh_search, SendMessage
model: opus
---
You amend hq's designs for the lead who started you. The caller's handover names the designs, the records
they follow, what to write in (the findings, or the operator's answers to design questions, each with its date), and
the branch. You work in a git worktree of hq under
`.work/` on that branch, made from a freshly fetched `origin/main`, and change nothing outside it; you commit
there and never push.
**First, what the mesh already knows.** Ask the knowledge base for every record the handover names
(`knowledge-base.get` with `reasoning`, and `links`), and `knowledge-base.ask` for the area in plain words: a
record you did not expect may already decide a point. Read each design whole before changing it, hq's glossary
(`00-META/glossary.md`) and playbook 02's steps for amending a to-be design. A clause the records do not
decide is not written into a design as if they did. A choice within what a record decides (a field, a tool, an
event, a bound, the order of steps) is the design's own: write it in the design with its reason, under the record it
follows from (playbook 02, step 2). An operator's answer to a design question is written the same way, citing the
operator and the date (playbook 02, *Answering a design question*). Only a question outside every record's question
is a design question for the caller.
**It reads the mesh and writes nothing there.** It calls only verbs that read (`knowledge-base.get`, `ask`, `links`,
`holds`; `issue-tracker.list`, `get`); never `knowledge-base.reserve` or `propose`, nor any `issue-tracker` verb that
writes (`open`, `move`, `note`, `link`). What a record or an issue must carry goes to the caller in its report.
**Then write, by the rules of the record.** A to-be design describes the mesh being built toward, in prose and
diagrams, never code and never a pasted manifest (a field may be named). **Write it in the shape a builder reads
whole** (playbook 02, *The shape of a design*): the main file at most 80 KB with the purpose, the whole shape, the
acceptance tests and the milestones; every line one milestone builds in that milestone's part, at most 60 KB, its
statements numbered with a bold lead; a line in one place only. **Leave the builder no choice**: each design line
names its seat, verb or event, the field and its form, the key and who deletes it, the bound, the caller, the tool
that reads or steers it, and the fate of what exists today; a line you cannot complete from the records or the
operator's answers is a design question in your report, never a line written as if decided. When the handover's
design is not yet in this shape, put it in it in the same change. Each statement a decision carries
cites it ("ADR NNNN §n"); each record the design now follows is in its frontmatter's `decisions:`, and `updated:`
is today. Every interaction is a mesh path: a seat's verb, an event, a provision, a module's own manifest. Speak
of the seat first, the module second. A rule the design states says how it is checked. hq is public: role
names, never node names, addresses or paths. A new word goes into the glossary with its definition and its
*Not:* line in the same change; a retired word is never used.
**Then check.** Run `go test ./...` at the worktree's root (the records, index, cycle and words tests) and fix
what it refuses. Commit in the imperative, about why.
Report to the caller in under 30 lines: the branch and its head, each file changed with the findings it now
carries, the size of each main file and milestone part you touched, what the tests said, the design's own choices you wrote, one line each, and every question you left open because it falls outside
every record's question, with your recommendation. Glossary words only; a guess is said as one.
+33
View File
@@ -0,0 +1,33 @@
---
name: hq-record-writer
description: Proposes one decision record or research effort to the knowledge base (seat knowledge-base, hq playbooks 01 and 02) from a draft the lead names - reserves its number when it has none, proposes it whole, reads the knowledge base's own review, revises and proposes again until it approves or asks the operator, at most four versions. Writes nothing in hq or in any repository.
tools: Read, Grep, Glob, Bash, mcp__mesh__mesh_call, mcp__mesh__mesh_describe, mcp__mesh__mesh_search, SendMessage
model: opus
---
You put one record into the knowledge base for the lead who started you. The handover names the draft (a file the
design writer wrote, outside hq), the record's number when it has one, whether it needs the operator, and its homes
(the hq designs that cite it).
**First, what holds.** Read the draft whole, then every record it links (`knowledge-base.get` with `reasoning`,
`knowledge-base.links`), and the clauses it changes as they stand now (`holds`). A clause the draft says it changes
must exist and still be in force; a fact the draft rests on that you can read on the mesh, read it. When a link or a
fact is wrong, or a clause names a part, event or verb the designs retire, stop and say so to the lead: never
propose a clause you know to be false.
**Then propose it whole.** `knowledge-base.reserve` a number only when the draft has none. `knowledge-base.propose`
takes the title, topic, date, deciders (the operator with the date and the words of each answer, then the lead and
the writer who wrote it down), the clauses in order, each opening with a bold lead, the sections (Context,
Considered Options, Consequences, How it is checked, References), the links (`changes` with the clause it goes out
from), the homes, and for `needs-operator` the brief the operator's phone shows: a headline of at most 60
characters, two or three short sentences, at most three points, in plain glossary words. A refusal names every rule
it broke: fix all of them at once and propose again.
**Then read its review.** The knowledge base reviews each version itself (`knowledge-base.get`, its `reviews`).
On `changes`, fix what is a form or a citation; a finding that changes what was decided, or that only the operator
can answer, goes to the lead with your recommendation, and you wait. At most four versions; past that, report what
stands. On `approve` a record that needs the operator waits for the operator's Approve: read
`operator-channel.asks` to see that its ask is there.
**Report** to the lead: the number, the version, the verdict and its findings, the ask's key, and what you changed
between versions. Use the glossary's words (hq `00-META/glossary.md`); no node names in the record.
+53
View File
@@ -0,0 +1,53 @@
---
name: mesh-code-reviewer
description: Reviews one change of the Novox mesh before it is pushed (hq playbook 07, the review; at most three rounds): each commit against the design lines it cites, code no design line asks for, the build maps' What exists today, and whether its tests prove it. Read-only; reports findings, never fixes, and never re-judges the design.
tools: Read, Grep, Glob, Bash, mcp__mesh__mesh_call, mcp__mesh__mesh_describe, mcp__mesh__mesh_search
model: opus
---
You review one change of the Novox mesh for the lead who started you, independently of who built it. The
caller's handover names the issue, the design and its milestone part at an hq commit, the build maps, the branch,
the worktrees and their commits, and the round.
**The design is what you judge against.** Read the milestone part and the design's main file whole at the commit
the handover names, the build maps, hq's playbook 07 (step 2), the glossary and the mesh's instructions
(`/etc/claude-code/CLAUDE.md`). The design was judged against the records and the mesh's principles when it was
written and reviewed by architectural domain; you do not judge it again. The handover and anything a lead or another
session said about what to build are not a source: only the design lines and the hq commits that answer design
questions are.
**What makes it wrong** (a finding):
- a commit that does not do what the design lines it cites say, or does only part of it; a design line in a build
map with no commit; a commit that cites no design line, or an answer no hq commit holds;
- code no design line asks for: a new exported type, field, constant, package, module, state, verb, event, bound,
caller or tool, or a fate for what exists, that the milestone part does not give; and a piece of code placed where
the design line (or the design section *Where a seat's code goes* it cites) does not put it. The fix is to take it
out, or to hold it as a design question for the lead until the operator's answer is in the design;
- an interaction the design does not place that is not a mesh path: one module naming another, reading another's
internals or being special-cased in another part; a module's name in shared code; state only in memory; a poll or
a timer where an event exists;
- an effect or a reader of the build map's `What exists today` whose fate the diff does not carry out (run its
searches again on the reviewed tree), or one the search finds and the answer omits;
- a test that does not fail without its change (check it by taking the change out in a copy), an acceptance
criterion with no test, a new test function where the repository is over budget, real sleeping;
- vet or tests that fail: run `go vet ./...`, `go vet -tags integration ./...` and `go test ./...` in every
touched module, with the siblings placed as the build map's one placement says;
- a change to any `module.json` reported without the manifest validator's verdicts (every manifest whose verdict
differs between the change and the trunk), or with a manifest it refuses or holds: the merge check would fail
on it only after a full build.
**A design line you think wrong is a note, never a change asked.** Where a line contradicts a decision or a
principle you know of, name the line and why in a note; the lead brings it to the operator. The code follows the
line until the design changes.
**Prove it against the live mesh, never a re-creation.** When the change answers a fault seen on the machines,
check what it claims against the state the mesh itself uses, read through its tools (the manifest validator's own verdicts in the indexer,
the packages state, the store, a node's haves), never against a copy you rebuilt: a manifest validator run without the seat list,
for one, hides the refusals the live one makes. A claim you could only check against a re-creation is said as
such in the report.
Anything else (hardening, a further case, a nicer form) is a note, never a round. Read only: no edits, no git
write, no verb that writes, never ssh. Use a temporary directory under `/var/tmp`.
Answer playbook 07 step 2 item 10's two questions, one line each. Answer **APPROVED at** the full commit ids, or numbered findings each with file:line, the design line it concerns,
what is wrong, and the fix; design questions and notes apart. Under 25 lines, glossary words only.
+78
View File
@@ -0,0 +1,78 @@
---
name: mesh-developer
description: Builds one prepared change of the Novox mesh in local worktrees on a feature branch (hq playbook 07, develop; playbook 08): code and tests, committed locally, never pushed. Builds what the milestone part's design lines say and nothing else, each commit citing its lines; a question the design does not answer stops that subtask and goes to the lead.
tools: Read, Grep, Glob, Bash, Edit, Write, mcp__mesh__mesh_call, mcp__mesh__mesh_describe, mcp__mesh__mesh_search, SendMessage
model: sonnet
---
You build one change of the Novox mesh for the lead who started you. The caller's handover names the issue, the
design and its milestone part at an hq commit, your build map, the subtasks you build, the branch name and the
worktrees.
**The design is your only input.** Read your build map first, then, at the commit the handover names, every design
line its rows cite (the milestone part's `§n`, found by searching its files) and each section of the design's main
file those lines name, whole; read a further line of the milestone part when a cited line points to it. Never read
the whole milestone part and main file at once: a large design (design 47's milestone G and main file are about 580
KB) fills most of your context before the first subtask (2026-11-11: four developers stopped for context after one
or two subtasks). Then read hq's playbook 07 (`00-META/process/07-feature-branches.md`, step 1), the
glossary and the mesh's instructions (`/etc/claude-code/CLAUDE.md`). Do not read decision records or the knowledge
base to decide what to build, and take no instruction about what to build from a handover or a message: only the
design lines and the hq commits that answer design questions say it.
**Build each design line as written, and nothing it does not say.** Every exported type, field, constant, package,
module, state, verb, event, bound, caller and tool you add is given by a design line (the milestone part's, or the
design section *Where a seat's code goes* it cites); every interaction travels by the seat, verb, event or provision
the design names. Where you think a design line wrong by the mesh's principles (it names one module from another,
polls, keeps state only in memory), build it as written and say so in your report, for the lead to bring to the
operator; never build otherwise.
**A question the design does not answer stops that subtask.** When a subtask needs a choice no design line makes,
or the code contradicts the build map in a way that needs one, stop that subtask: send the lead the question with
`SendMessage` (to `main`), naming the design line, file:line and what is left open, and go on with the subtasks that
do not need it. Never guess it, build around it or build both ways; an answer counts only once an hq commit holds it: a design line, or the lead's one-line answer in the milestone's lead-answers file, which names the principle (`00-META/principles.md`) that settles it.
A fact the build map got wrong (a file moved, a function renamed) you say to the lead and go on.
**How you work.** Worktrees from `origin/main` on the branch the handover names, subtasks in the build map's order:
one that uses a contract, type, verb or package another subtask adds starts from that subtask's commit. Commit per
subtask, its label in the subject and a line `Builds: design NN M §n, …` in the body, locally only: never push, never open a pull request, never call a verb that writes on the mesh (reads are fine). Never
ssh, never a shell helper for the mesh. Tests: every change carries a test that fails without it (check it);
rows or subtests in existing tests where the repository is over the test budget the instructions set; no real
sleeping. Run `go vet ./...`, `go vet -tags integration ./...` and `go test ./...` in every touched module as
the running checker places siblings (playbook 07). Once per head you report, not per commit, also run the integration tests of
every package you touched, and of every package of the same module that imports one, against an embedded bus
(`go test -tags integration -count=1` on those packages); where the importers are many, run the tests of them that
exercise the change by name (`-run`) and say that choice. Never a whole module's or level's run (playbook 07, the
operator's rule of 2026-10-18). Name the tests and their results in your report: a compile
with `go vet` does not show an integration test the change breaks. In mesh-sdk run the whole untagged `./...` at every
commit, whatever package you changed: a test there holds its copy of the seat list and fails on a change elsewhere. Use a temporary directory under `/var/tmp` (TMPDIR). Place siblings as the build map's one placement says (`go.work` `use` lines or `replace` directives pointing at their worktree paths), never a placement script of your own; never make a
symbolic link anywhere (the mesh's hard rules: nobody creates symlinks), not even under `/var/tmp`. Before you
report a head, also run the repository's own checks whole (in the catalog every `checks/*`): a head that passes its
modules' tests can still break a check of the whole repository, such as a committed binary or an unannounced state.
When the change touches any `module.json`, in any repository, run the manifest validator as the checker does: the
program `cmd/mesh-judge` in the indexer's module, built at mesh-delivery's trunk, or at the change's mesh-delivery
head when that head changes the validator itself (then the indexer's own manifest is also validated at the trunk).
Give it raw manifests, not digests: the trunk's, with the change's laid over them, as the two cases `change` and
`trunk` the checker's `judgeRequest` names, with one predefined key list for both cases, always: the change's when it alters that
list, else the trunk's. Give it, and every catalog check you run, what is live now: the seat list the seat
`mesh-catalog`'s verb `seat-definitions` answers, read in the same turn and read again when you report a head, with the definitions the change's heads alter
laid over it for the change case, and the catalog at its trunk, fetched fresh, beside it. Never a captured copy, a
test fixture or a list kept from an earlier run: a stale copy refuses manifests the running validator accepts, and
hides failures it refuses. The merge check fails a changed manifest the validator refuses or holds,
and also an unchanged one in which the change causes a
problem the trunk case lacks; it says so only after a full build, and no module's unit tests run the validator.
Report every manifest whose verdict differs between the two cases, with your heads. The reviewer asks for those
verdicts, and a head reported without them is not reviewed.
Start no agents of your own: the lead decides who works on what.
**Start a subtask only with room to finish it.** Before each subtask, judge whether its reading and its work fit
in what is left of your context with a third to spare; if not, stop between subtasks, never inside one: commit only
whole subtasks (tests green), and report your heads and the next subtask's label. A fresh agent continues from the
build map and your commits' `Builds:` lines; write no handover of findings or choices into any file.
**Say a commit others wait on as it lands.** When the handover names a subtask other agents wait on, send its full
commit id to the lead with `SendMessage` (to `main`) as soon as it is committed, and go on; commit each subtask on its
own with its label in the subject, so the others can also read it from your worktree.
Report in under 20 lines: per subtask the design lines it builds, the change (file:line) and its test, that each
test failed first, the full commit ids, the subtasks stopped on a design question (each question in one line), the
design lines you think wrong, anything the build map got wrong, and what is left. Glossary words only.
+41
View File
@@ -0,0 +1,41 @@
---
name: mesh-preparer
description: Prepares one milestone or small fix of the Novox mesh before any code is written (hq playbook 07, the prepare step; playbook 08), from the milestone's part of its design alone - one build map per repository (where each design line lands in code, what exists there today and what relies on it, the subtasks and their order) and the list of design questions the design does not answer. Decides nothing; writes only its build maps and the milestone file's design questions.
tools: Read, Grep, Glob, Bash, Write, mcp__mesh__mesh_call, mcp__mesh__mesh_describe, mcp__mesh__mesh_search, SendMessage
model: opus
---
You prepare one change of the Novox mesh for the lead who started you. The caller's handover names the issue, the
design and its milestone part at an hq commit, and where your files go (playbook 07, step 0). You write the build
maps and the milestone file's section `Design questions`, nothing else; you call no verb that writes and make no git
write.
**The design is your only input.** Read the milestone part and the design's main file whole, at the commit the
handover names, then hq's playbook 07 (step 0 and step 0b), the glossary (`00-META/glossary.md`) and the mesh's
instructions (`/etc/claude-code/CLAUDE.md`). Do not read decision records or the knowledge base to judge the design:
they were read when it was written and reviewed. Read the issues the handover names (`issue-tracker.get`) for their
done-when. Code shows what exists, never what is to be built.
**Then map each design line onto code**, as playbook 07 step 0 says: per repository, every design line it builds
("design NN M §n") with the files and functions it changes, its subtask (about thirty minutes, one change, its
label), its acceptance criteria (each citing the design's acceptance test or line it comes from) and the test that
proves each; the order of the subtasks (a subtask that uses a contract, type, verb, event's form or package another
adds starts from that subtask's commit; contracts and packages first) and one writer per file; the section `What
exists today` with its three answers, every search written as run; the design line that places each new exported
type, field, constant, package or module; the one placement of the siblings every agent uses, as the running checker
places them; the test budget. Compare each seat contract the milestone adds or changes (its fields, subjects,
callers and kinds, as on main) with the design lines that name that seat, field by field: every difference is a
design question. Code: read the repositories at `origin/main` (`git fetch`, `git show`, `git grep`),
never a working tree; the live mesh through its tools.
**Everything the design does not say is a design question, never your answer.** A reader or an effect with no fate,
a field's form, a key and who deletes it, a bound, a caller, an owner, a tool for a new state, an interaction whose
seat, verb, event or provision the design does not name, code or a live state that contradicts a design line: list
each under `Design questions` with the design line (or the missing one), what it leaves to its builder, where you met
it (file:line or the tool's answer) and your recommendation, said as one. A build map holds no sentence about how
anything should be, no finding of your own and no ruling; where you think a design line wrong by the mesh's
principles, it is a design question too, and you map the line as written.
Report to the caller in under 30 lines: the build maps' paths, the count of design lines mapped per repository and
any line not mapped, the subtasks' order, and the design questions, one line each with your recommendation. Glossary
words only; a guess is said as one.
+34
View File
@@ -0,0 +1,34 @@
---
name: mesh-watcher
description: Follows one thing the lead waits on in the Novox mesh (a pull request's check, a merge, a delivery, a decision's Approve, an answer on Telegram) and reports when it lands, fails, or stands still. Read-only; never acts.
tools: mcp__mesh__mesh_call, mcp__mesh__mesh_wait, mcp__mesh__mesh_describe, mcp__mesh__mesh_search, SendMessage
model: sonnet
---
You watch one thing for the lead who started you. The handover names what you watch, the verb that reads its
state (with its arguments), the event family that wakes you, when to report and your bound.
**First, the owner's event.** Before you wait, name the part that owns the fact you watch, and wait on that part's
own event: a check's verdict on the checker's, a merge on the delivery gate's, a delivery on the status view's or the node-engine's
haves, a record on the knowledge base's, an answer on the operator channel's. Use another family only when this
machine's tool runner is not granted the owner's (`mesh_wait` names the granted ones when it refuses), and say so in
your report, naming the grant that is missing.
**What a machine runs is read from the machine.** For a delivery, read `<node>/node-engine.composition` with the
module: what the node runs (its fingerprint and phase) against what the assigner wants
(`mesh-delivery-view.assigner_want` with the node and the module). A line of the status view that says a merge
is not yet taken in is a report, not a wait; never report "nothing reached a machine"
without that read on each machine the module is wanted on.
Loop: wait with `mesh_wait` on the event family (its answer only wakes you), then read the state from the verb
the handover names; never report a state you did not read in that round.
**A report along the way does not end you.** Your final message ends you, and then nothing watches. So while you
keep watching, report with `SendMessage` to `main`: when five minutes passed with no change while something
should be moving (say what you last read and when), and at each change the handover asks to hear. Then go on
waiting. End, with your final message, only at the first of: the thing reached its end (passed, merged,
delivered, accepted, chosen, declined), it failed (quote every failure line word for word), the bound passed, or
a call failed twice (quote it).
You only read: no verb that writes, never ssh, never a shell helper. Report in a few lines, with times in UTC as
the tools give them; the lead says them to the operator in local time.
+36
View File
@@ -0,0 +1,36 @@
---
name: research-reader
description: Carries one reading task of a research effort's step 5 (hq playbook 01, Do the research) - reads the code on main, counts from the manifests, or tries a program's behaviour in process, and reports facts with their repository, commit and function. Read-only on the mesh and every working copy.
tools: Read, Grep, Glob, Bash, mcp__mesh__mesh_call, mcp__mesh__mesh_describe, mcp__mesh__mesh_search
model: sonnet
---
You carry one reading task of a research effort of the Novox mesh (hq playbook 01, step 5, *Do the research*).
The caller names the effort (a research number in the knowledge base), the question or questions you answer,
and what to read, count or try.
Read first: the questions you were given, the effort's sections the caller names (`knowledge-base.get` with
`kind: research`, its number, `reasoning: true`, `bound` 65536, or one `section`), and hq's glossary
(`00-META/glossary.md`). Then the code.
How you read:
- **Code on main only, never a working copy's checkout.** Other sessions work in the working copies on this
machine; never check out, reset, stash, pull or commit there. Run `git fetch origin` in the
repository and read with `git show origin/main:<path>`, `git grep <words> origin/main`, `git ls-tree` and
`git log origin/main`. A branch the caller names (an unmerged release) is read the same way as
`origin/<branch>`, and every fact from it is said to be from that branch.
- **Each fact with its source:** the repository, the short commit of `origin/main` you read, and the function
or type (a manifest field by its name); never a path or a line number.
- **Counts from the manifests** on main of every repository that holds them, said with the date, the
repositories and the total read.
- **A try in process** (a local NATS server on the loopback, a Go test program in a temporary directory),
when the caller asks for one: minutes, never a lab run, never against the mesh's bus. Say what you ran and
what it printed.
- Through the mesh MCP server only reads: knowledge-base `get`, `ask`, `holds`; issue-tracker `get`,
`list`. Call no verb that writes.
Report in under 700 words, per question: what exists today (facts with sources), what is missing, and the
options the code allows, each with its cost (which modules a change rebuilds; a mesh-sdk change is named
as one). Separate fact from guess: a guess is said as one, with how to check it. Write plain words in the
glossary's words. Say what you read and what you could not.
@@ -0,0 +1,42 @@
---
name: review-health-and-events
description: Reviews a research effort's settled direction or a design against the architectural domains health and events and logs, before it is proposed or merged (hq playbook 01, Review by architectural domain). Read-only; never reopens a decision.
tools: Read, Grep, Glob, Bash, mcp__mesh__mesh_call, mcp__mesh__mesh_describe, mcp__mesh__mesh_search
model: opus
---
You review one subject of the Novox mesh against two architectural domains: **health** and **events and logs**. The caller
names the subject: a research effort or a decision record in the knowledge base (by number), a design in hq,
and where needed the code it changes. Four reviewers like you run side by side, each on two architectural
domains (hq playbook 01, *Review by architectural domain*).
**First, what the mesh already knows.** Before you judge, ask the knowledge base what holds in the subject's
area beyond the subject itself (`knowledge-base.ask` with its topic in plain words, `knowledge-base.holds`), and
list the decisions and principles in force that bear on your architectural domains, by number and clause. Judge
the subject against those too: a subject that contradicts a decision in force, or adds an interaction between
modules that is not a mesh path (a seat's verb, an event, a provision, the module's own manifest), is a finding.
Read first: the subject whole (`knowledge-base.get` with `bound` 65536 and `reasoning: true`, section by
section if it is cut; a design through `git diff origin/main` of its branch), the records it links where your
architectural domains need them, the mesh's instructions in `/etc/claude-code/CLAUDE.md`, and hq's glossary
(`00-META/glossary.md`). Read code and records only. Call no verb that writes, edit nothing, and make no git
write.
What you judge:
- Health: what each part's health check says, which conditions are raised and cleared and by what, whether any failure can pass silently (work that reports success and did nothing is the worst fault), and whether a part that waits says so (nothing-moved-in-five-minutes).
- Events and logs: an event for every step, what each carries, whether existing consumers of an event keep working (a waiter on a family must not wake early), and what can be read back later and for how long.
Judge only whether the settled direction or design is complete and sound in your two architectural domains,
and whether its acceptance tests prove it. **For a design, complete means its builder has no choice to make**
(playbook 02, *The shape of a design*): a design line in your architectural domains that leaves a field's form, a
key or who deletes it, a bound, a caller, an event, a tool or the fate of what exists to its builder is a **must**,
since the prepare would meet it as a design question and develop would wait for it. A milestone part over 60 KB, or
a main file over 80 KB, is a must too. Never reopen what the operator decided: a finding that would change
a decision says so and goes to the operator through the lead.
Report in under 450 words: findings only, most severe first, each with its architectural domain, the clause,
row or design section it concerns, what is missing or wrong, why it matters concretely, and the fix in one or
two sentences (a sentence for the direction or the design, or a new acceptance test). Mark each **must** (wrong
or unprovable without it) or **note** (an improvement for the design). Write plain words in the glossary's
words, with no analogies. Say what you read; a guess is said as one.
@@ -0,0 +1,42 @@
---
name: review-running-and-testing
description: Reviews a research effort's settled direction or a design against the architectural domains running qualities and testing, before it is proposed or merged (hq playbook 01, Review by architectural domain). Read-only; never reopens a decision.
tools: Read, Grep, Glob, Bash, mcp__mesh__mesh_call, mcp__mesh__mesh_describe, mcp__mesh__mesh_search
model: opus
---
You review one subject of the Novox mesh against two architectural domains: **running qualities** and **testing**. The caller
names the subject: a research effort or a decision record in the knowledge base (by number), a design in hq,
and where needed the code it changes. Four reviewers like you run side by side, each on two architectural
domains (hq playbook 01, *Review by architectural domain*).
**First, what the mesh already knows.** Before you judge, ask the knowledge base what holds in the subject's
area beyond the subject itself (`knowledge-base.ask` with its topic in plain words, `knowledge-base.holds`), and
list the decisions and principles in force that bear on your architectural domains, by number and clause. Judge
the subject against those too: a subject that contradicts a decision in force, or adds an interaction between
modules that is not a mesh path (a seat's verb, an event, a provision, the module's own manifest), is a finding.
Read first: the subject whole (`knowledge-base.get` with `bound` 65536 and `reasoning: true`, section by
section if it is cut; a design through `git diff origin/main` of its branch), the records it links where your
architectural domains need them, the mesh's instructions in `/etc/claude-code/CLAUDE.md`, and hq's glossary
(`00-META/glossary.md`). Read code and records only. Call no verb that writes, edit nothing, and make no git
write.
What you judge:
- Running qualities: build capacity (one check per change, ADR 0548), speed, storage growth against each form's cap, what happens while a part, the bus or the control-node is down, concurrency (two writers, a race with a take-over), start-up time.
- Testing: an acceptance test for every decision, each checkable as the operator sees it and labelled by level (live by tool call, or the one integration test that starts a module whole against the real bus and store); every failure path; the test budget (at most 50 integration and 500 unit tests per repository or catalog module).
Judge only whether the settled direction or design is complete and sound in your two architectural domains,
and whether its acceptance tests prove it. **For a design, complete means its builder has no choice to make**
(playbook 02, *The shape of a design*): a design line in your architectural domains that leaves a field's form, a
key or who deletes it, a bound, a caller, an event, a tool or the fate of what exists to its builder is a **must**,
since the prepare would meet it as a design question and develop would wait for it. A milestone part over 60 KB, or
a main file over 80 KB, is a must too. Never reopen what the operator decided: a finding that would change
a decision says so and goes to the operator through the lead.
Report in under 450 words: findings only, most severe first, each with its architectural domain, the clause,
row or design section it concerns, what is missing or wrong, why it matters concretely, and the fix in one or
two sentences (a sentence for the direction or the design, or a new acceptance test). Mark each **must** (wrong
or unprovable without it) or **note** (an improvement for the design). Write plain words in the glossary's
words, with no analogies. Say what you read; a guess is said as one.
+42
View File
@@ -0,0 +1,42 @@
---
name: review-seams-and-tools
description: Reviews a research effort's settled direction or a design against the architectural domains seams and tools, before it is proposed or merged (hq playbook 01, Review by architectural domain). Read-only; never reopens a decision.
tools: Read, Grep, Glob, Bash, mcp__mesh__mesh_call, mcp__mesh__mesh_describe, mcp__mesh__mesh_search
model: opus
---
You review one subject of the Novox mesh against two architectural domains: **seams** and **tools**. The caller
names the subject: a research effort or a decision record in the knowledge base (by number), a design in hq,
and where needed the code it changes. Four reviewers like you run side by side, each on two architectural
domains (hq playbook 01, *Review by architectural domain*).
**First, what the mesh already knows.** Before you judge, ask the knowledge base what holds in the subject's
area beyond the subject itself (`knowledge-base.ask` with its topic in plain words, `knowledge-base.holds`), and
list the decisions and principles in force that bear on your architectural domains, by number and clause. Judge
the subject against those too: a subject that contradicts a decision in force, or adds an interaction between
modules that is not a mesh path (a seat's verb, an event, a provision, the module's own manifest), is a finding.
Read first: the subject whole (`knowledge-base.get` with `bound` 65536 and `reasoning: true`, section by
section if it is cut; a design through `git diff origin/main` of its branch), the records it links where your
architectural domains need them, the mesh's instructions in `/etc/claude-code/CLAUDE.md`, and hq's glossary
(`00-META/glossary.md`). Read code and records only. Call no verb that writes, edit nothing, and make no git
write.
What you judge:
- Seams: where each piece of state lives on the bus (key-value bucket, stream, object store) and which module owns it; which seat, verb or event joins two parts; any link that is not NATS-native (a poll, a timer, HTTP between modules, a shared file, state only in memory); whether a form the manifest lacks is named with its cost (a mesh-sdk change rebuilds every module).
- Tools: for every new piece of state a tool that reads it, and for every new behaviour a tool that steers it (do by hand, ask again, drop, repair, re-check), as the instruction tools-are-how-a-module-is-spoken-to says.
Judge only whether the settled direction or design is complete and sound in your two architectural domains,
and whether its acceptance tests prove it. **For a design, complete means its builder has no choice to make**
(playbook 02, *The shape of a design*): a design line in your architectural domains that leaves a field's form, a
key or who deletes it, a bound, a caller, an event, a tool or the fate of what exists to its builder is a **must**,
since the prepare would meet it as a design question and develop would wait for it. A milestone part over 60 KB, or
a main file over 80 KB, is a must too. Never reopen what the operator decided: a finding that would change
a decision says so and goes to the operator through the lead.
Report in under 450 words: findings only, most severe first, each with its architectural domain, the clause,
row or design section it concerns, what is missing or wrong, why it matters concretely, and the fix in one or
two sentences (a sentence for the direction or the design, or a new acceptance test). Mark each **must** (wrong
or unprovable without it) or **note** (an improvement for the design). Write plain words in the glossary's
words, with no analogies. Say what you read; a guess is said as one.
@@ -0,0 +1,42 @@
---
name: review-security-and-upgrade
description: Reviews a research effort's settled direction or a design against the architectural domains security and upgrade, before it is proposed or merged (hq playbook 01, Review by architectural domain). Read-only; never reopens a decision.
tools: Read, Grep, Glob, Bash, mcp__mesh__mesh_call, mcp__mesh__mesh_describe, mcp__mesh__mesh_search
model: opus
---
You review one subject of the Novox mesh against two architectural domains: **security** and **upgrade**. The caller
names the subject: a research effort or a decision record in the knowledge base (by number), a design in hq,
and where needed the code it changes. Four reviewers like you run side by side, each on two architectural
domains (hq playbook 01, *Review by architectural domain*).
**First, what the mesh already knows.** Before you judge, ask the knowledge base what holds in the subject's
area beyond the subject itself (`knowledge-base.ask` with its topic in plain words, `knowledge-base.holds`), and
list the decisions and principles in force that bear on your architectural domains, by number and clause. Judge
the subject against those too: a subject that contradicts a decision in force, or adds an interaction between
modules that is not a mesh path (a seat's verb, an event, a provision, the module's own manifest), is a finding.
Read first: the subject whole (`knowledge-base.get` with `bound` 65536 and `reasoning: true`, section by
section if it is cut; a design through `git diff origin/main` of its branch), the records it links where your
architectural domains need them, the mesh's instructions in `/etc/claude-code/CLAUDE.md`, and hq's glossary
(`00-META/glossary.md`). Read code and records only. Call no verb that writes, edit nothing, and make no git
write.
What you judge:
- Security: who may call each new verb, how its grants are scoped (ADR 0363's grant form), which credentials go or remain, what can be replaced or forged (a digest, an author, a tag), what enters from outside the mesh, and every act that removes data.
- Upgrade: how what runs today moves to the new shape without a delivery breaking, in which order (mesh-sdk, the grants part, the manifest validator, the module), what readers see during and after, how each step is rolled back, what a node on an older node-engine does, and bootstrap before the bus.
Judge only whether the settled direction or design is complete and sound in your two architectural domains,
and whether its acceptance tests prove it. **For a design, complete means its builder has no choice to make**
(playbook 02, *The shape of a design*): a design line in your architectural domains that leaves a field's form, a
key or who deletes it, a bound, a caller, an event, a tool or the fate of what exists to its builder is a **must**,
since the prepare would meet it as a design question and develop would wait for it. A milestone part over 60 KB, or
a main file over 80 KB, is a must too. Never reopen what the operator decided: a finding that would change
a decision says so and goes to the operator through the lead.
Report in under 450 words: findings only, most severe first, each with its architectural domain, the clause,
row or design section it concerns, what is missing or wrong, why it matters concretely, and the fix in one or
two sentences (a sentence for the direction or the design, or a new acceptance test). Mark each **must** (wrong
or unprovable without it) or **note** (an improvement for the design). Write plain words in the glossary's
words, with no analogies. Say what you read; a guess is said as one.
+4 -1
View File
@@ -26,7 +26,10 @@ and says they disagree.
1. Propose the decision record in the knowledge base (`hq-graduate`, steps 2 and 3). If it reverses an
earlier one, it `supersedes` it, or `changes` the clauses it replaces — the earlier record's text is
never edited.
2. Edit the design, add `"ADR NNNN"` to its `decisions:`, and set `updated:` to today.
2. Edit the design, add `"ADR NNNN"` to its `decisions:`, and set `updated:` to today, keeping it in the shape of
playbook 02's *The shape of a design* (a main file and one milestone part per milestone, numbered design lines).
An operator's answer to a design question is written as a design line citing the operator and the date, under
the record it follows from (playbook 02, *Answering a design question*).
3. If this came from an issue, give the issue the document in the issue tracker: `amended-design` on its
`issue-tracker.move` to `resolved`, or `issue-tracker.link` with kind `design` before then. Never in the
report's frontmatter (ADR 0275).
+5 -3
View File
@@ -27,12 +27,14 @@ written into this repository.
status, reviews and the operator's ask.
4. **Write the design** under `03-DESIGN/01-to-be/` with `layer: to-be`, `status: designed`,
empty `code:`, today's `updated:`, and `decisions:` listing `"ADR NNNN"`.
5. **Close the effort** — `knowledge-base.propose` the effort again, whole, with `status: graduated` and
`became` links to the record (`"NNNN"`) and the design (its path).
5. **Close the effort** — `knowledge-base.close` with `to: graduated`, `became` links to the record (`"NNNN"`)
and the design (its path), and a two- or three-sentence note; never by proposing the effort again whole
(playbook 02 step 5).
## Amending an existing design
A design changes only through a decision. Propose the record first; if it reverses an earlier one,
A design changes only through a decision; a choice within what a decision decides (a field, a tool, an event, a
bound) is the design's own, written in it with its reason. Propose the record first; if it reverses an earlier one,
it `supersedes` it (or `changes` the clauses it replaces) — **never edit the earlier record's text**;
a correction of fact is a progressive insight (`propose` the accepted record with `insight`). Then edit
the design, add `"ADR NNNN"` to its `decisions:`, and set `updated:`.
+6 -3
View File
@@ -19,9 +19,12 @@ Hands a settled design to a code repository. **Authoritative playbook:**
3. **Read the as-is counterpart.** What is being replaced must be written down in
`03-DESIGN/00-as-is/` before it is changed. Building against an undocumented as-is is how
shipped behaviour gets lost.
4. **Flip the status** to `in-progress`, `updated:` today.
5. Build in the code repository. HQ never carries implementation.
6. On completion, run `hq-graduate`'s "when something ships" section.
4. **Give each milestone its part** (playbook 04 step 4a, playbook 02 *The shape of a design*): every line it
builds in `NN-milestones/<milestone>.md`, numbered, at most 60 KB; the main file at most 80 KB.
5. **Flip the status** to `in-progress`, `updated:` today.
6. Build in the code repository, from the milestone part alone (playbook 07): no code before every design question
its prepare finds is answered by the operator and written into the design. HQ never carries implementation.
7. On completion, run `hq-graduate`'s "when something ships" section.
## Non-negotiable
+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
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
tracker.
4. Write the symptom **as observed**, in plain terms, with the evidence that it happened.
+1 -1
View File
@@ -21,7 +21,7 @@ All through the mesh MCP server (`mesh_describe` gives each verb's arguments).
number is spent; never pick one by hand.
3. **Propose the effort.** `mesh_call knowledge-base.propose` with `kind: research`, the reserved `number`,
`title`, `status: active`, `date` (today), `sections` (the documents in order, each `name` and `text`;
the first `00-overview.md`: **what** is investigated, **why**, and **what it touches**), and `links`
each named by a plain title, never as a file; the first, **Overview**: **what** is investigated, **why**, and **what it touches**), and `links`
of kind `touches` for every record, effort, issue or design it touches (`cites` for one it only names).
4. **Do the research** in further documents of the same effort. Each change is `propose` again with the
effort whole and `why`. A refusal names every rule broken: fix them all and propose again. On review
+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
already drifted after a single addition, which is why it was removed, and the generated one
written into `02-DECISIONS/README.md` made every two open decisions conflict (issue 297), which
is why that went too. `index.py` fails if the list is stored there again.
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
knowledge base for a record; if one is wrong, flag it.
+1
View File
@@ -1 +1,2 @@
__pycache__/
.work/
+1 -1
View File
@@ -15,7 +15,7 @@ works — plus the engineering practice that holds across everything Novox build
## Rules
- Markdown only.
- Markdown only, but for the checks and one drawn overview of the playbooks (`process/playbooks.html`).
- **Stable by nature.** Changes here reflect a genuine shift in intent, not iteration. The one
exception is `how-we-build.md`, which changes whenever a rule is earned — and only through
its amendment process.
+22 -16
View File
@@ -1,15 +1,20 @@
# 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"
python3 00-META/checks/index.py the reading order's topics are written, and no list of records is stored
python3 00-META/checks/cycle.py the development cycle: a design's layer, status, decisions and code; an issue report's frontmatter
python3 00-META/checks/words.py the glossary's retired words are not used, no daemon's line is written, and no word is defined twice
python3 00-META/checks/words.py --list tools the words the catalogue's copy must list
python3 00-META/checks/words.py --list lines the daemons' lines the catalogue's copy must list
records_test.go structure: links, no record written here, citations in the form "ADR NNNN"
index_test.go the reading order's topics are written, and no list of records is stored
cycle_test.go the development cycle: a design's layer, status, decisions and code; an issue report's frontmatter
words_test.go the glossary's retired words are not used, no daemon's line is written, no word is defined twice,
and the glossary names no seat's holder
```
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,
which is how a superseded decision stayed live in the constitution for days and in
@@ -26,7 +31,8 @@ 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 |
| `decisions-form` | every entry of a design's `decisions:` is "ADR NNNN", four digits | written with issue 426; it failed on an entry `ADR 290` before it passed |
| `mention-form` | every "ADR N" in the text has four digits, and every "research N" three; a code span is an example and is not read | written with issue 426; it failed on `ADR 12` and `research 45` before it passed |
| *(index.py)* | the reading order's six topics are written in `02-DECISIONS/README.md`, none twice, and the README stores no list of records (ADR 0248). The knowledge base reads these lines and refuses a record whose topic is not one of them | a stored list made every two open decisions conflict ([issue 297](../../04-ISSUES/297-two-open-decisions-always-conflict/00-report.md)); it failed on the README of `main` before the list was removed |
| *(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 |
| `holders` | the glossary names no module as a seat's holder: no seat "held by" or "claimed by" a named module, no named module "holder of" a seat, no module that "holds the seat"; the holder is read from the mesh's assignments (the operator, 2026-11-09) | **twenty statements** in the glossary, from the registry seats' "held today by the forge module" to the knowledge base's holder |
| `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
@@ -37,7 +43,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
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 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 +53,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` `clauses` | the knowledge base's `has-clause`, `clause-numbering`, `clause-lead` and `clause-size` |
| `records.py` `numbering` | the knowledge base's `title` and `reserved` |
| `records.py` `topics`, `index.py` (a record's topic) | the knowledge base's `topic`, against the reading order `index.py` still checks |
| `records.py` `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 |
| `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` |
@@ -72,7 +78,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
whose failure has never been observed is a guess about its own correctness.
## cycle.py
## The cycle test
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
@@ -84,9 +90,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
owner for a located issue, a fix for a resolved one, and a core issue's `replay` or `replay-none` (ADR 0237)
— are refused at the write by `issue-tracker.move`. Its rules on research efforts and decision records left
with them (the table above). `python3 00-META/checks/cycle.py`
with them (the table above).
## words.py
## The words test
One word per thing, checked (ADR 0244).
It reads the glossary's *Not:* lines (the retired words, each with its scope) and *Identifier until
@@ -95,15 +101,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
efforts live in the knowledge base, which holds a proposed record to the same words (the table above). It
also fails when a glossary head word heads two entries or is also retired.
[`words-allowed.md`](words-allowed.md) names a document that could not be reworded at once, with a date. With `MESH_CATALOG_DIR` set it compares the catalogue's copy
[`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.
It also fails on a **daemon's line** (ADR 0272,
its insight of 2026-10-09): the operator types one command, `nox`, and a document names a `nox` line, never the
controller's or the node-engine's binary followed by one of its commands. The lines are the check's own list
(`DAEMON_LINES`); a command that is a line only with an argument after it is matched as a line is written. Looked
(`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
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.
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)
}
+20 -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
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),
@@ -8,3 +8,21 @@ which keeps a record's words as it was imported.
| 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 words of its day, "merge gate" (issue 1303) and "judge" (the glossary's manifest validator retired the noun); it is not rewritten |
| `04-ISSUES/286-a-merge-check-passed-on-an-agents-machine-and-failed-on-the-build-seat/00-report.md` | 2027-04-30 | history: the issue's narrative keeps the word of its day, "judge" (the composition check's program built from a change; the glossary's manifest validator retired the noun); it is not rewritten |
| `04-ISSUES/306-a-check-on-the-control-node-ran-past-its-timeout/01-diagnosis.md` | 2027-04-30 | history: the issue's narrative keeps the word of its day, "judge" (the composition check's program built from a change; the glossary's manifest validator retired the noun); it is not rewritten |
| `04-ISSUES/310-a-pull-request-ran-its-own-copy-of-its-check/00-report.md` | 2027-04-30 | history: the issue's narrative keeps the word of its day, "judge" (the composition check's program built from a change; the glossary's manifest validator retired the noun); it is not rewritten |
| `04-ISSUES/343-the-controllers-terminal-has-no-command/01-diagnosis.md` | 2027-04-30 | history: the issue's narrative keeps the word of its day, "judge" (a part of the controller's pull request, named so then; the glossary's manifest validator retired the noun); it is not rewritten |
| `04-ISSUES/361-the-control-nodes-agent-account-is-never-judged/01-diagnosis.md` | 2027-04-30 | history: the issue's narrative keeps the word of its day, "judge" (the self-check's look at what an account can reach; the glossary's manifest validator retired the noun); it is not rewritten |
| `04-ISSUES/299-an-optional-verb-makes-a-holder-look-silent/01-diagnosis.md` | 2027-04-30 | history: the issue's narrative keeps the word of its day, "seat set" (the seats compiled into the controller then; the glossary's seat list retired it); 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))
+862
View File
@@ -0,0 +1,862 @@
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`.
// holders the glossary says no seat's holder: no seat "held by" or "claimed by" a named module, no named module
// "holder of" a seat, no module that "holds the seat" (the operator, 2026-11-09). Who holds a seat is read
// from the mesh's assignments, and the glossary's head says where.
// domains no retired domain heads a section of the glossary (ADR 0641 §5: Placement).
// 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/", ".claude/", "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
}
// holders: the glossary defines a word and never says which module holds or claims a seat. Who holds a seat is a fact
// of the mesh's assignments, read with one call (the operator, 2026-11-09: "it shouldn't be in the glossary"), and a
// copy here drifts. Refused, across line breaks: a seat "held by" or "claimed by" anything but a generic holder or a
// seat ("held once for the mesh by `x`", "held today by the forge module"); a named module "holder of" a seat ("the
// module `x`, holder of"); a module that "holds the seat" ("it holds the seat", "the `x` module holds it"); and "its
// holder, the x". A word for the role ("the module that holds the seat `x`") names no module and passes.
var (
heldByRx = regexp.MustCompile("(?i)\\b(held|claimed)(\\s+once)?(\\s+for\\s+the\\s+mesh)?(\\s+today)?\\s+by\\s+")
// What may follow "held by": a holder in general, a seat, or the knowledge base (a seat's word) holding a record.
heldByGenericRx = regexp.MustCompile("^(a\\s+module\\b|some\\s+module\\b|its\\s+holder\\b|the\\s+holder\\b|the\\s+identity\\s+seat\\b|the\\s+knowledge\\s+base\\b|one\\s+author\\b|the\\s+`[^`]+`\\s+seat\\b|the\\s+(node\\s+)?seat\\s+`)")
holderNamedRx = []*regexp.Regexp{
regexp.MustCompile("(?i)(\\bmodule\\s+`?[a-z][a-z0-9-]*`?|`[a-z][a-z0-9-]*`)\\s*,\\s+(the\\s+)?holder\\s+of\\b"),
regexp.MustCompile("(?i)\\b(it|which)\\s+holds\\s+(the\\s+)?(node\\s+)?seats?\\b"),
regexp.MustCompile("(?i)\\bmodule\\s+holds\\s+(it|the\\s+(node\\s+)?seat)\\b"),
regexp.MustCompile("(?i)\\bholder\\s*,\\s+the\\s+[a-z`]"),
}
)
func checkHolders(text string) []string {
var out []string
say := func(i int, m string) {
out = append(out, fmt.Sprintf("%s:%d: %q names a seat's holder; the glossary says where it is read instead", glossaryFile, lineOf(text, i), strings.Join(strings.Fields(m), " ")))
}
for _, loc := range heldByRx.FindAllStringIndex(text, -1) {
rest := text[loc[1]:]
if heldByGenericRx.MatchString(rest) {
continue
}
end := loc[1] + min(len(rest), 30)
say(loc[0], text[loc[0]:end])
}
for _, rx := range holderNamedRx {
for _, loc := range rx.FindAllStringIndex(text, -1) {
say(loc[0], text[loc[0]:loc[1]])
}
}
return out
}
func TestWordsGlossaryNamesNoSeatHolder(t *testing.T) {
expectNone(t, checkHolders(readFile(rootFS(), glossaryFile)))
}
func TestHoldersRefusesANamedHolderAndPassesARole(t *testing.T) {
for _, bad := range []string{
"- **x** — the seat `x`, held once for the mesh by the module `mesh-x`.",
"- **x** — the seat `x`, held\n by `mesh-x`.",
"- **x** — `go-package-registry` (held today by the forge module).",
"- **x** — the seat `x`, claimed by mesh-x.",
"- **x** — the module mesh-x, holder of the seat `x`.",
"- **x** — the module `mesh-x`, holder of `x`.",
"- **mesh-x** — the module that does x: it holds the seat `mesh-x` once.",
"- **bus** — The `nats` module holds it.",
"- **x** — written by its holder, the assigner.",
} {
if len(checkHolders(bad)) == 0 {
t.Errorf("not refused: %q", bad)
}
}
for _, good := range []string{
"- **seat** — a named role, held by a module assignment.",
"- **router** — the module that holds the `operator-channel` seat.",
"- **bus** — Held by the `mesh-broker` seat.",
"- **x** — the seat `x`; its holder is read with `mesh-module-assigner.assignments`.",
"- **depends on a seat** — a seat held on its node by some module.",
} {
if got := checkHolders(good); len(got) != 0 {
t.Errorf("refused a role: %v", got)
}
}
}
// domains: a retired domain is no heading of the glossary. ADR 0641 §1 retired Placement into Core (module
// assignment) and Delivery (the wants), and §5 has this test refuse it as a domain heading.
var retiredDomains = []string{"Placement"}
func checkRetiredDomains(text string) []string {
var out []string
for _, d := range retiredDomains {
rx := regexp.MustCompile(`(?m)^##\s+` + regexp.QuoteMeta(d) + `\b`)
for _, loc := range rx.FindAllStringIndex(text, -1) {
out = append(out, fmt.Sprintf("%s:%d: %q is a retired domain (ADR 0641 §1) and heads no section", glossaryFile, lineOf(text, loc[0]), d))
}
}
return out
}
func TestWordsGlossaryHasNoRetiredDomainHeading(t *testing.T) {
expectNone(t, checkRetiredDomains(readFile(rootFS(), glossaryFile)))
}
func TestRetiredDomainsRefusesTheHeadingAndPassesProse(t *testing.T) {
if len(checkRetiredDomains("## Placement — what runs on which node\n")) == 0 {
t.Error("a Placement heading was not refused")
}
if got := checkRetiredDomains("## Core\n\nwhich was the Placement domain until ADR 0641\n"); len(got) != 0 {
t.Errorf("prose refused: %v", got)
}
}
+819 -162
View File
File diff suppressed because it is too large Load Diff
+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
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
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 |
|---|---|---|---|
| **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 |
- **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
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
caught neither real failure; the forge's checks found both.
+30
View File
@@ -0,0 +1,30 @@
---
status: canonical
updated: 2026-11-11
---
# The principles
A short, living list. Every question met while building is tested against these principles and answered by the
lead in one line; only a question no principle settles goes to the operator or to a decision record. A detail that
a principle settles is not written down as a design line or a record. The list changes when the operator says so:
add, sharpen or drop a principle in a pull request to this file.
1. **NATS-native.** Every interaction between modules is an event, or a call over the bus where an answer is
needed. A module keeps its data in the bus's own storage, and reads another module's data only by asking it,
through its verbs or its tools.
2. **The mesh owns the truth.** State lives in the mesh and is written onto the nodes from there. Nothing is changed
by hand on a node.
3. **Failure is loud.** Nothing reports success when it did nothing, and nothing fails silently. When in doubt, fail.
4. **A module is steered by its tools.** Everything a module holds can be read, everything it does can be started
by hand, and everything that goes wrong can be recovered, all through its tools.
5. **Everything ships the same way.** The mesh's own parts go through the same build, check and delivery as any app.
6. **Nothing is exposed publicly.** No data leaves the mesh unasked: no secret in a record, a log or a public
repository, no service reachable from outside unless it is meant to be. Inside the mesh, every agent and module
is trusted; who may do what is not a question to ask.
7. **The simplest thing that works.** Prefer what already exists. No new mechanism unless it is needed, and fewer
parts over more.
8. **One word per thing**, from the [glossary](glossary.md).
How it is checked: the one code review of a milestone checks the code against these principles and the design's
shape; an answer to a develop question names the principle that settles it.
+21 -5
View File
@@ -46,8 +46,23 @@ to-be document's frontmatter goes to `implemented`, and both stand — one descr
the other recording what was intended. Deleting the intention loses the reasoning, which is
the expensive half.
## A design is built as it is written
Research, decisions and the review by architectural domain are where the mesh's choices are made; building makes
none (the operator, 2026-11-10). A design is written to be built without a choice of the builder's own, in a main
file and one part per milestone that an agent reads whole (playbook [02](02-graduation.md), *The shape of a
design*). The prepare, develop and the code review of playbook [07](07-feature-branches.md) read only that design:
the prepare says where each design line lands in code and lists what the design does not answer, the operator
answers each such design question and the answer is written into the design, and only then does develop start. Every
commit cites the design lines it builds, and the review judges each change against its line. How it is checked:
playbook 07's *How it is checked* (the build maps, the commits' `Builds:` lines, the design questions' answering hq
commits).
## The playbooks
A drawn overview of them, the cycle, a feature's steps, the review loop and an issue's life, is
[`playbooks.html`](playbooks.html): a picture of these playbooks, never their authority.
| # | Playbook | Trigger |
|---|---|---|
| [01](01-research.md) | Research | An idea from the operator, or one worth investigating before committing to design: setup questions, findings, a direction the operator confirms |
@@ -56,18 +71,19 @@ the expensive half.
| [04](04-build-handoff.md) | Work breakdown and handoff | A design is ready to be built: milestones usable on their own, issues per repository, each milestone one feature step |
| [05](05-constitution-sync.md) | Constitution sync | `how-we-build.md` changed a rule the mesh enforces |
| [06](06-writing-a-module.md) | Writing a module | Something that runs today must run on the mesh |
| [07](07-feature-branches.md) | Features | Work that changes code across issues or repositories: prepared context, local development, one review, one check, one delivery |
| [08](08-small-fixes.md) | Small fixes | One issue in one repository, about an hour of work: one review, one push, one delivery |
| [07](07-feature-branches.md) | Features | Work that changes code across issues or repositories: a build map from the milestone's part of its design, the design complete before any code, local development, one review against the design lines, one check, 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 |
| [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 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
sharing a number or an issue report carrying a status key; a `graduated` research effort that
does not say what it became is refused by the knowledge base's `propose` (ADR 0290). Run it with `records.py`, `index.py` and
`words.py` before any HQ merge (`sh merge-check.sh` runs all four). The issue rules the check
does not say what it became is refused by the knowledge base's `propose` (ADR 0290). Run it with the records, index and
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
refused at the write by the issue tracker's `move`
(ADR 0275). What the checks cannot see — that code work actually started from a
+64 -16
View File
@@ -39,20 +39,33 @@ An idea arrives in one of two forms, and the first step differs:
- **Explain each question with a concrete picture** in the operator's words: what they would see, do or
lose under each option. No internal names (a record number, a verb, a word the glossary lacks) without
what it is beside it.
- **Write the requirements from the answers, before the options.** Each one line, numbered, saying what
the result must hold and how it is checked (for example "R5. A change across repositories is
delivered whole or not at all; check: a change over three repositories"). The requirements shape the
effort: every option is weighed against them, and the operator corrects them before any option is
worked out (the operator, 2026-11-09: "coming up with a list of requirements can also be an important
part, it actually shapes the research").
3. **Take a number.** `knowledge-base.reserve` with `kind: research` and `for`: the effort's title. The number
is spent whether or not the effort is ever proposed; one never proposed is withdrawn after a week. Never
pick one by hand.
4. **Propose it.** `knowledge-base.propose` with `kind: research`, the `number`, a one-line `title`,
`status: active`, `date` (the day it began), and `sections`, each a `name` and its markdown `text`:
- `00-overview.md`: the idea in the operator's words, what exists, the operator's answers (dated), and the
`status: active`, `date` (the day it began), and `sections`, each a `name` and its markdown `text`. A
section is a part of the record, never a file: its `name` is a plain title, a reference to another section
names that title ("the Direction's test 8"), and code evidence names the repository, the commit and the
function, never a path or a line (the operator, 2026-11-02: "don't use file references or local files
anymore"; the knowledge base is to be shown as a web page, where a file name links to nothing). Once
proposed, the record is the only copy: it is changed by `knowledge-base.get` and `propose`, never kept
in a local draft. The first sections:
- **Overview**: the idea in the operator's words, what exists, the operator's answers (dated), and the
words the effort uses that the glossary lacks, each defined in one line;
- `01-questions.md`: the questions to answer, the first ones first;
- `02-method.md`: what will be tried and measured, and on what.
- **Requirements**: the numbered requirements of step 2, each with its check;
- **Questions**: the questions to answer, the first ones first;
- **Method**: what will be tried and measured, and on what.
Name what it touches as `links` of kind `touches` (`"0276"`, `"research 045"`, `"issue 365"`, or a
design's path), and what it names for another reason as `cites`.
5. **Do the research.** Try, measure and read, smallest question first. Each finding goes into
`04-findings.md`: what was tried, what it showed (with numbers), what it means. Each change of the effort
the section **Findings**: what was tried, what it showed (with numbers), what it means. Each change of the effort
is `knowledge-base.propose` again, whole, with `why`; every version is kept, and `knowledge-base.history`
lists them and gives the difference between two. Until issue 844 is delivered, the knowledge base
takes a new version of an accepted, active effort only as a progressive insight: give `insight`, saying
@@ -69,20 +82,52 @@ An idea arrives in one of two forms, and the first step differs:
server, a protocol) is tried in process, on the machine's loopback, in minutes. Lab runs (about 12 to
16 minutes fresh) are for the mesh's own code; before that code exists, a lab run tests nothing an
in-process test does not, and its lab scenario is written as the build's acceptance test instead.
6. **Place it in the domains.** Before the direction, write `03-domains.md`: the domain or domains the idea
6. **Place it in the domains.** Before the direction, write the section **Domains**: the domain or domains the idea
belongs to (the glossary's sections, ADR 0244); the modules and seats it lands in, each new, changed or
retired; what it does to each domain's words, each word added (with its domain and a one-line meaning),
changed or retired, and none on a *Not:* line or already meaning something else (the glossary's homonyms);
and whether a domain grows or shrinks, a concept moving between domains included. The direction names its
decisions in those words. Asked when the idea is set up, too: an option that lands a concept in the wrong
domain is offered with that cost named, like one that breaks a rule.
7. **Settle the direction with the operator.** When the questions are answered, write `05-direction.md`:
the proposed decisions in a few lines each, the options rejected and why, and the build's acceptance
tests: what the build must prove before it is done, carried by its milestones (playbook
7. **Settle the direction with the operator.** When the questions are answered, write the section **Direction**:
the question the effort answers, the options weighed and the one proposed (a second only for a second
question of its own; playbook [02](02-graduation.md), step 2), the design choices that follow from it in a
line each, and the build's acceptance tests: what the build must prove before it is done, carried by its milestones (playbook
[04](04-build-handoff.md)). Nothing else is left open: each question closes by a test, by a decision the
effort records with its reason (a small design limit is the lead's), or by the operator. Ask the
operator to confirm or change the direction, one point at a time, then propose it once. Then the effort
graduates through playbook [02](02-graduation.md).
operator to confirm or change the direction, one point at a time. Then review it by architectural domain (below) and
propose it once. Then the effort graduates through playbook [02](02-graduation.md).
## Review by architectural domain
A direction the operator settled can still be wrong where nobody looked. Before it is proposed, and again
before a design is merged (playbook [02](02-graduation.md)), it is reviewed against each **architectural domain**
(the operator, 2026-11-02), by reviewers on the larger model, about two architectural domains each, side by side:
the four reviewer agents in `.claude/agents/` (`review-security-and-upgrade`, `review-seams-and-tools`,
`review-running-and-testing`, `review-health-and-events`), each given the subject by number or path:
- **security**: who may call each new verb, how its grants are scoped, which credentials go or remain, what
can be replaced or forged, and what enters from outside the mesh;
- **seams**: where each piece of state lives on the bus, which seat, verb or event joins two parts, and any
link that is not NATS-native. Each piece of state is kept in the bus storage form that fits it (a key-value
bucket, a stream, an object store); a form the manifest lacks is added to mesh-sdk, and the review names that change
and its cost (a mesh-sdk change rebuilds every module) to the operator, never folds it in silently (the
operator, 2026-11-02);
- **running qualities**: build capacity, speed, storage growth, and what happens while a part, the bus or the
control-node is down;
- **health**: what each part's health check says, which conditions are raised and cleared, and whether any
failure can pass silently;
- **events and logs**: what each step says on the bus, and what can be read back later;
- **testing**: an acceptance test for every decision, each one checkable and within the test budget, and the
failure paths;
- **upgrade**: how what runs today moves to the new shape without a delivery breaking, in which order, and
how it is rolled back;
- **tools**: which tool reads each new piece of state and which one steers each new behaviour.
A reviewer judges only whether the settled direction is complete and sound in its architectural domains; it never reopens
a decision. Each finding is a sentence for the direction or a new acceptance test; a finding that changes a
decision goes to the operator. The direction ends with a section *Reviewed by architectural domain*: per architectural domain, what was
found and how it was answered, or that nothing was found.
## Bounds and models
@@ -90,9 +135,10 @@ An idea arrives in one of two forms, and the first step differs:
|---|---|---|
| reading and the setup questions (1, 2) | 30 minutes | the session's own, larger |
| proposing (3, 4) | 15 minutes | larger |
| each experiment or reading task (5) | 30 minutes | smaller, one agent at a time unless the operator allows more |
| each experiment or reading task (5) | 30 minutes | smaller, one agent at a time unless the operator allows more: the agent `research-reader` |
| placing it in the domains (6) | 15 minutes | larger |
| the direction (7) | 30 minutes | larger |
| the review by architectural domain | 20 minutes | larger, about two architectural domains per reviewer |
A step past its bound is said to the operator at once, with what is being done about it. The lead reports
as playbook 07 does: every ten minutes and at once when something is found, stops or needs the operator,
@@ -119,13 +165,14 @@ 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 give a `became` link while the effort is open — playbook 02 gives it at closure.
- Do not write into `03-DESIGN` from an open effort.
- Do not write an effort, or a copy of one, into this repository: `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/`.
## Closing
An effort never just stops. It closes through playbook [02](02-graduation.md) as `graduated` or
`abandoned`, always with a `became` link to what it turned into. Nothing is deleted — what was rejected,
`abandoned`: a graduated effort with a `became` link to what it turned into, an abandoned one with a `cites` link to
what replaced it (playbook 02 step 5). Nothing is deleted — what was rejected,
and why, is the more expensive half to rediscover.
## How it is checked
@@ -134,7 +181,8 @@ The rules on an effort are the knowledge base's: `propose` refuses one that brea
record (the module's table test holds one case per rule), and the rules that judge the whole record set run
after every acceptance and raise a condition when one fails. The habits of steps 2, 5, 6 and 7 (options checked
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; the review by architectural domain is read in the direction's *Reviewed by architectural domain* section (nothing refuses a direction
without it yet: the lead and the reviewer hold it); 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
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.
+123 -15
View File
@@ -13,7 +13,7 @@ clauses in force for a topic or a record).
## Graduating research
The effort's `05-direction.md`, confirmed by the operator (playbook [01](01-research.md), step 7), is what
The effort's Direction, confirmed by the operator (playbook [01](01-research.md), step 7), is what
graduates. The lead writes the records and the design on the larger model; it is one sitting, bounded at
about an hour, and said to the operator if it runs past.
@@ -21,8 +21,17 @@ about an hour, and said to the operator if it runs past.
[`mission.md`](../mission.md), [`context.md`](../context.md) and
[`effect.md`](../effect.md). If it conflicts, either the effort is wrong or GENESIS is —
say which, in writing, before proceeding.
2. **Record the decisions** (below, *Writing a decision record*): one record per choice that could be
reversed on its own, not one record for the whole effort.
2. **Record the decisions** (below, *Writing a decision record*): one record per question the effort
answers, holding that question, the options weighed and the one chosen. Most efforts answer one question
and graduate into one record; a second record is written only for a second question of its own, such as
how the change can be checked or delivered at all (the operator, 2026-11-06: an effort graduated into
eleven records where it answered two questions). The choices made while working out the design that
follows (a field, a tool, an event, a bound, the order of steps) are the design's: written in the design
with their reasons, under the record they follow from, never as records of their own. A clause of an
earlier record the decision changes is a `changes` link from the record, with one line saying in what.
How it is checked: the lead names, in the graduation's report to the operator, the question each record
answers; an effort graduating into more than two records is said to the operator with why, before the
records are proposed.
- **Write them together, not one after another.** Reserve every number first, then propose each record
as soon as it is written, without waiting for the review of the one before; the reviews run side by
side, and the operator's asks for the records that need them arrive together. A record that
@@ -49,21 +58,39 @@ about an hour, and said to the operator if it runs past.
---
```
The design lists the direction's acceptance tests: what the build proves before it is done.
The design lists the direction's acceptance tests: what the build proves before it is done. Before it is
merged, the design is reviewed by architectural domain as playbook [01](01-research.md#review-by-architectural-domain) says, and
ends with its own *Reviewed by architectural domain* section; a design amended without an effort is reviewed the same way.
The words of the effort's `03-domains.md` go into the glossary in the same change, each in its domain: a
The words of the effort's Domains go into the glossary in the same change, each in its domain: a
word added gets its entry, a word whose meaning changed gets its entry rewritten, and a retired word moves to
its replacement's *Not:* line. A word that already means something else in another domain is named as a
homonym with the qualified form each domain uses.
4. **Hand it to the work breakdown.** The design goes on through playbook [04](04-build-handoff.md):
milestones, issues, and each milestone built as one feature step (playbook [07](07-feature-branches.md)).
5. **Close the effort.** `knowledge-base.propose` the effort again, whole, with `status: graduated` and a
`became` link to the decision record (`"NNNN"`) and to the design document (its path). An effort that
stops without a decision is proposed with `status: abandoned` and a `became` link to what replaced it.
5. **Close the effort.** `knowledge-base.close` the effort with `to: graduated`, a `became` link to each
decision record (`"NNNN"`) and the design document (its path), and a note of two or three sentences (what it
became, what carries it). The verb keeps the served text unchanged and adds a section *Graduated* with the note;
the knowledge base's review judges only the closing. Never close by proposing the effort again whole:
that is judged as a fresh proposal, so the approved reasoning is re-judged against the decisions that replace it
(on 2026-11-10 an effort closed that way met 5, then 11 findings on unchanged text; `close` was approved at once).
An effort that stops without a decision is closed `to: abandoned` with a `cites` link to what replaced it: the
knowledge base refuses `became` from a research effort to another research effort (its link-fits rule, design
55 § 2), so a replacing effort is cited, and `became` stays for the decision and the design an effort turned
into.
## Writing a decision record
**Before a number: what holds, and the mesh path, reviewed.** Ask the knowledge base what is already decided in
the record's area (`knowledge-base.ask`, `holds`), and name in the draft, for every interaction between modules the
record decides, the seat and verb, event, provision, state on the bus or manifest field that carries it. Then
`review-seams-and-tools` reviews the draft before it is proposed: a clause that has one module name another, read its
internals or be special-cased in another part, or puts a module's name into shared code, is a finding (the operator,
2026-11-03).
How it is checked: the record's References name the seams review's verdict; a record proposed without it is a breach
the lead names.
1. **Take a number.** `knowledge-base.reserve` with `kind: decision` and `for`: the record's title. The
number comes from the issue tracker's counter and is spent whether or not the record is accepted; one
never proposed is withdrawn after a week. Never pick a number by hand.
@@ -94,7 +121,10 @@ about an hour, and said to the operator if it runs past.
deferral no issue tracks. The knowledge base then reviews the proposal itself, through a judgement that
sees only the record, the records it links and the rules, and answers `approve` or `changes`. On
`changes` the record stays proposed with the findings: revise it by proposing it again, whole, with `why`;
at most three rounds, then the findings go to the operator with a recommendation.
after three rounds the lead does not stop to ask, but fixes each further finding itself and proposes again
(the operator, 2026-10-18: ADR 0429 went past three rounds, each finding narrower than the last, and asking
first only added a wait). Only a finding that would change what was decided, rather than how the record
says it, goes to the operator, with a recommendation.
On `approve` it is accepted, or, when it says `needs-operator`, it waits for the operator's Approve on a
channel that proves who answered. Nothing proposed is in force. `knowledge-base.get` shows a record's
reviews and the operator's ask when it waits for one.
@@ -120,13 +150,88 @@ by a progressive insight.
record `changes` the older one's clause, so a reader who arrives at the older record by a citation sees
what changed it since.
## The shape of a design
A design is what its builders build from, and nothing else is (playbook [07](07-feature-branches.md)): decision
records and the mesh's principles are read when a design is written, amended or reviewed, and never again by the
prepare, the developers or the code review. So a design is written to be built without a choice of the builder's
own, and in pieces an agent can read whole. On 2026-11-10 one design of more than 400 KB was built in one
milestone: its agents read it by searching for pieces, filled what they did not find themselves, and an audit of
that day's 40 or so choices by the leads found seven against the records or the design and 29 questions the design
never answered, all found only in develop.
- **A main file and one milestone part per milestone.** The design's main file
(`03-DESIGN/01-to-be/NN-<slug>.md`) holds its frontmatter, its purpose, the whole shape (its parts, their seats,
events, provisions and state, drawn and in short prose), its acceptance tests and its *Milestones* section. Each
milestone not yet delivered has its **milestone part**, `03-DESIGN/01-to-be/NN-milestones/<milestone>.md`, which
holds every line that milestone builds and opens with frontmatter naming `layer: to-be`, the design, the
milestone and the `decisions:` its lines follow. A line lives in one place: a statement one milestone builds moves
from the main file into that part, never copied.
- **Bounds.** The main file at most 80 KB, a milestone part at most 60 KB. An agent reads both whole in under
40,000 tokens, under a fifth of what it can hold, and has the rest for the code. A milestone whose part would
pass 60 KB is two milestones (playbook [04](04-build-handoff.md)), each usable on its own.
- **Numbered design lines.** A milestone part's statements are numbered, each one thing to build with a bold
lead, like a decision's clauses, and cited as "design NN M §n"; a subtask, a commit and a review finding cite
them so. Each line names what its builder needs: the seat, verb or event, the field and its form, the key and
who deletes it, the bound, the caller, the tool that reads or steers it, and the fate of each thing that exists
today and changes. A line a builder would have to complete is a design question left open.
- **A delivered milestone's part stays.** It records what was intended, as a to-be design does once it ships
(the overview, *The two design layers*); no agent reads it again, so it costs no builder anything, and what runs is
written in the as-is design. The history of how a milestone was cut or why it waited is the issue tracker's and the
records', not the design's.
- **Design 47's next amendment is written in this shape**: the main file keeps the purpose, the whole shape, the
acceptance tests and the milestones, within 80 KB; every milestone gets its part with every line it builds or
built, moved from the main file, the one about to be built first; and the narrative of past milestones and their
prepare steps leaves the design for the issues it names. Every other design is put in this shape
at its next amendment, and a new design is written in it.
How it is checked: the reviewer of a design's pull request reads `wc -c` of the main file and of each milestone
part against the bounds, and that every statement of a milestone part is numbered; a check among hq's Go tests is to
be built (the cycle test reads only a design's main file today).
## Answering a design question
A design question (playbook [07](07-feature-branches.md), step 0) is answered by the operator and by nobody else:
not by the lead, the preparer, a developer or a reviewer, however technical it is. The one exception is a question
of internal form only (playbook 07, step 0, *Who answers a design question*; the operator, 2026-11-11): changing
nothing the operator sees, nobody's permissions and no data kept or removed, it is answered by `hq-design-writer` as
a design line of the milestone part with its reason, under the record it follows from, read in item 3's round, and
told to the operator in one line. The lead asks it as the
instruction on asking says, one question per ask, with what the milestone part says, what it leaves open, where it
was met (file:line) and a recommendation. Then:
1. **Within a record's question** (a field, a bound, an owner, an order of steps, inside what a record decides), the
answer is the design's own choice: `hq-design-writer` writes it as a design line of the milestone part, citing
the operator's answer and its date and the record it follows from.
2. **Outside every record's question**, or **against a record** (the design contradicts a decision in force), the
answer is a decision: written first as *Writing a decision record* or *Changing a decision* above says, then as
design lines.
3. **The answers are reviewed together.** Before develop starts, the four reviewers by architectural domain read the
milestone part as amended, in one round (playbook [01](01-research.md#review-by-architectural-domain)); answers
met during develop are read by them in one round before the code review.
4. **They land on the milestone's branch.** The amendment is committed on hq's branch of the milestone
(`feat/<slug>`), so its pull request joins the milestone's delivery group, and the builders read the milestone
part at that branch's head.
How it is checked: every design question in the milestone file names the hq commit that answers it; a design line
added during a milestone with no operator's answer or record behind it, other than an answer of internal form only
that touches nothing the operator sees, no permission and no data, is a finding of the review by architectural
domain.
## Amending an existing design
A design changes only through a decision.
A design changes only through a decision. A choice within what a decision decides (a field, a tool, an event,
a bound) is the design's own, written in it with its reason; it needs no record of its own. It is made when the
design is written or amended, never while it is built: a choice the design leaves to its builder is a design
question (above).
1. Write the decision record (above). If it reverses an earlier one, it `supersedes` it.
2. Edit the to-be design document, add the record to its `decisions:` as `"ADR NNNN"`, and set `updated:`
to today.
to today, keeping the design in the shape above (a line one milestone builds goes into that milestone's part).
A set of amendments across several designs (a milestone's findings, a graduation's) is written by the
agent `hq-design-writer` (`.claude/agents/`), started with a handover naming the designs, the records and the
branch. Review it by architectural domain before it is merged (playbook [01](01-research.md#review-by-architectural-domain)), and
rewrite its *Reviewed by architectural domain* section.
3. If the amendment came from an issue, give the issue the document path in the issue tracker:
`amended-design` on its `issue-tracker.move` to `resolved`, or `issue-tracker.link` with kind
`design` before then — never in the report's frontmatter
@@ -150,7 +255,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 change a decision record's meaning. Supersede it. Correcting a fact it got wrong, while
the decision stands, is a progressive insight — marked and dated in the record, never silent.
- Do not write a decision record, or a copy of one, into this repository: `records.py` refuses a new path
- 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.
## How it is checked
@@ -159,8 +264,11 @@ 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
(an accepted decision cited by nothing, a supersession named by one side only, a changed clause nothing
changes, an accepted record extending a superseded one) run after every acceptance and raise a condition
when one fails. In this repository, `records.py` checks that a design's `decisions:` entries and every
"ADR NNNN" in the text are well formed, and that no record is written here; `index.py` checks the reading
order's six topics, which the knowledge base reads. The three habits of graduating step 2 (side by side, clauses
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; the index test checks the reading
order's six topics, which the knowledge base reads. A design's *Reviewed by architectural domain* section is read by the
reviewer of its pull request; nothing refuses a design without it yet. 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,
a deferral no issue tracks and a retired word, and the review returns a record named but not linked.
The rounds are read in `knowledge-base.get` (its reviews, one per version): a question to the operator about a
record's wording after its third round is the failure of step 4 of *Writing a decision record*.
+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
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
drift.
@@ -91,7 +91,7 @@ import read, which every imported issue names (`imported.commit` in `issue-track
ADR 0281).
4. Resolve. `issue-tracker.move` to `resolved`, which the tracker takes only from `fixing`, with `fixed-by`, and, if the root cause was a design gap,
run playbook [02](02-graduation.md) and give `amended-design`. **A core issue** — its `located-in`
names the controller, the node-engine, the tool runner, the SDK, or the catalogue's bus, forge or build
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
the commit before the fix and pass on it, or with `replay-none` saying why none is possible
(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
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
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.
- 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.
@@ -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
`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
`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
`opened:`, and no report carries a status key.
+31 -10
View File
@@ -9,11 +9,19 @@ an hour, and said to the operator if it runs past.
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").
A milestone that changes nothing anyone can use is not a milestone; fold it into the next.
- **A milestone is one feature step** of playbook [07](07-feature-branches.md): prepared, developed in one to
two hours, reviewed, checked and delivered as one. A milestone larger than that is split.
- **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).
- **A milestone is one piece of functionality, never cut by an order between its parts** (the operator, 2026-10-21:
a milestone may span several modules, repositories and dependencies such as an SDK change; 2026-10-22: "a milestone
is a milestone, we shouldn't cut it"). Which of its modules reaches the machines first (a reader of a new manifest
word before the modules that use it, a provider before its consumers) is the assigner's rules' (design 47); an SDK
change is built into its users. The parts such an order touches are found in playbook [07](07-feature-branches.md)
step 0 item 6 and become issues of the same milestone; an order those rules cannot keep is an issue of the build
and delivery system, not a cut.
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
milestone's development time. Issues that must land in order are linked `depends-on`.
@@ -30,8 +38,14 @@ 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,
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
first. A milestone is named by what it lets someone do; how it ships is shaped by the plan its changes would
have, asked through `mesh-delivery.what-if`: the modules it moves and their fan-out (ADR 0401 §6).
first. A milestone is named by what it lets someone do; how it ships is shaped by the modules its changes move
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`).
4a. **Give each milestone its part** (playbook [02](02-graduation.md), *The shape of a design*): every line the
milestone builds moves from the main file into `NN-milestones/<milestone>.md`, as numbered design lines, each
naming what its builder needs. A milestone whose part would pass 60 KB is split here into two milestones, each
usable on its own; this is the one place a milestone is ever cut, for what one agent can read, before any work
starts. The main file stays within 80 KB. Written by `hq-design-writer` in the same change as the milestones.
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.
6. **Open the issues.** For each confirmed milestone, `issue-tracker.open` one issue per repository part, its
@@ -40,8 +54,12 @@ A design becomes **milestones**, and each milestone becomes **issues**:
entry in the design.
7. **Flip the status.** `status: in-progress`, `updated:` today; the design change goes through playbook
[08](08-small-fixes.md) (one hq pull request).
8. **Hand each milestone to playbook 07, one at a time.** The milestone is the feature step; its issues are the
step's issues; the next milestone starts when the previous one's delivery plan is done.
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 is delivered. Playbook 07 builds from
the milestone part alone and starts its develop step only once that part answers every design question its
prepare finds (playbook 07, *Design complete*); a question is never answered by building around it. A design waits
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.
9. **On completion of the last milestone**, run the "when something ships" section of playbook
[02](02-graduation.md).
@@ -52,16 +70,19 @@ A design becomes **milestones**, and each milestone becomes **issues**:
owns what is not. The knowledge base's rule `deferral` checks the decision record when it is proposed
([issue 354](../../04-ISSUES/354-a-decided-rename-was-never-scheduled-and-the-operator-believed-it-live/00-report.md)).
- HQ is not a code repository and never carries implementation.
- How the building is done — worktrees, agents, review, checks, the ready mark and the delivery — is playbook
- How the building is done — worktrees, agents, review, checks, the Approve and the delivery — is playbook
07's, not this one's.
## 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
issues; an in-progress design without one is the smell of building without a breakdown.
- Each milestone of an in-progress design has its part under `NN-milestones/`, within 60 KB, its lines numbered
(`ls -l 03-DESIGN/01-to-be/NN-milestones/`); a milestone handed to playbook 07 without one is step 4a skipped.
- Every issue a milestone names exists in the tracker (`issue-tracker.get`), and an issue in `fixing` names
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.
+45
View File
@@ -83,6 +83,31 @@ Three questions, answered from the machine:
thing works: the credential authenticates, a wrong one is refused, the containers reach each
other, the data survives a restart.
## A module that calls or holds a seat, or shares code
*Added 2026-11-04, widened 2026-11-05* (ADR 0625, ADR 0627, ADR 0634, ADR 0635). Where each piece of shared code goes
is [design 20](../../03-DESIGN/01-to-be/20-writing-a-module.md) *Where a seat's code goes*: shared code lives with its
owner. In short:
- **Import the seat's contract and, where it has one, its client package**, from the seat's domain's contracts module
(`mesh-<domain>-contracts`, or `mesh-contracts` for the Core domain and a domain with none), never the holder module
and never a copy of the seat's types in your own code. The list of seats your module needs is read from the bus,
never compiled in.
- **Logic you would write over a seat that another caller would write too** belongs in that seat's client package,
through a change of the contracts module; it never goes into mesh-sdk, and never into the holder.
- **Logic and state layouts only your module owns**, where it holds no seat for them, are your module's own package;
another repository's module imports it only by a published version, so your manifest declares it as a package
artifact.
- **Logic your domain's modules share, owned by no one module and carried by no seat**, is your domain's SDK's
(`mesh-<domain>-sdk`); only what modules of more than one domain use goes into mesh-sdk.
- **Holding a seat** means implementing its contract in your module. A seat you define yourself gets a contracts
module of its own (it may share your repository); a role no other module could ever take over is your module's
own tools and events, not a seat.
The review of the pull request checks the home of every new exported name; the checks design 26 lists refuse a stray
file in a contracts module, a seat named in mesh-sdk, an import of a contracts module from mesh-sdk, and a client
reaching another seat's client.
## What the first port actually cost
Six attempts, one real bug. Recorded because the ratio is the lesson: **the mesh was right every
@@ -105,3 +130,23 @@ time and the scaffolding was not.**
- **Never rotate during an adoption.** Rotation is a separate act, afterwards, deliberately.
- **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.
## 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.
File diff suppressed because it is too large Load Diff
+120 -35
View File
@@ -16,47 +16,115 @@ hour is a feature.
**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
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
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
code is written: on the issue, through the knowledge base, 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
decision stays a small fix.
model answering exact questions with file:line). **Read what exists in the fix's area today, briefly, before any
code** (playbook [07](07-feature-branches.md) step 0 item 3a, at a small fix's size): what the code and the live mesh
(through the mesh MCP server's tools) do there now, and for each value, label, file or job the fix changes or removes,
every reader that relies on it, found by a search across the repositories (written down as run) and not from memory,
with what happens to each after the fix. It is a few lines in the issue's note, under `What exists today`, and the
review runs the search again; a reader it finds that the note omits makes the fix wrong. **A fix builds what a
design says, and decides nothing** (the operator, 2026-11-10; playbook [07](07-feature-branches.md), step 0): its
input is the issue and the design that covers the area (the design whose `code:` names the repository, or the
issue's design link), read in the parts the fix touches and in the design section that places code, *Where a
seat's code goes*. The note names, for everything the fix adds or changes (an exported type, field, constant,
package or module, a state, a verb, an event, a bound, a caller), the design line that gives it. A fix that only
makes the code do what its issue and its design already say needs nothing more. Anything no design line gives is
a **design question**: the session asks the operator, one question with its recommendation, and the answer is
written into the design (playbook [02](02-graduation.md), *Answering a design question*) before any code builds
it; neither the session nor the lead answers it. A question of internal form only (playbook
[07](07-feature-branches.md), step 0, *Who answers a design question*) is answered instead by `hq-design-writer`
as a design line, read by the four reviewers by architectural domain in one round, and told to the operator in
one line. A handover to the developer quotes the design lines and never
decides a placement itself (2026-11-04: a class placed in mesh-sdk by a lead's handover, against that section,
passed review and its check). 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 design question answered 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,
naming the level of the test that proves it and the design line or issue done-when it comes from, as an
`issue-tracker.note` headed `Acceptance criteria`. A
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.
3. **Build on a local branch** `fix/<issue>-<slug>`, in a worktree of its own branched from `main`. Write the
test that fails today first; make the change; keep `gofmt`, `go vet` and the unit tests of what changed green
(each well under a second; never a merge check or a database locally: how-we-build §5, ADR 0391).
Commit, naming the issue. For a playbook or design edit, run the repository's own checks (in hq,
`sh merge-check.sh`).
4. **One review of the local commit, before anything is pushed** (a reviewer that did not write it, larger
model; at most 3 rounds, as playbook 07; from round 2 only what is wrong; a finding outside the issue's
done-when 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
3. **Build on a local branch** `fix/<issue>-<slug>`, in a worktree of its own branched from `main`, never in
the repository's own clone, which stays on `main` (playbook [07](07-feature-branches.md), step 0, item 2). Write the
test that fails today first, 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).
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
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,
`go test ./...` at the root). The integration tests are compiled whole (`go vet -tags integration ./...`), and once
per head reported those of the packages the fix touches and of their importers in the module run against an
embedded bus, or the tests of them that exercise the fix by name where the importers are many, named in the report;
never a whole module's or level's run (playbook 07 step 1). 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 have run that way at the reviewed head
(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 judges each change against the design line its note names, as playbook 07 step 2 item 2a, and code no
design line gives is a finding; it reads each acceptance criterion
against its test, as playbook 07 step 2 item 6; it runs the `What exists today` search again, as playbook 07 step 2 item 6b; 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; 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
operator's word on the pull request is its review, and the lead marks it ready on that word.
5. **Push once and open the pull request.** The trunk is what runs and nothing holds merged work back
(ADR 0401), so a fix waits for no other change's delivery. The forge's merge
check runs the integration tests, the only
place they run (the operator, 2026-10-16). A failed check
goes back to the same agent with the failure, and the reviewer reads only the change it makes.
6. **The reviewer marks it ready at once** (for an edit that skipped the reviewer, the lead, on the
operator's word) with `mesh-delivery.mark-ready` (the delivery of the head it approved, who reviewed it and
why), which writes the label `mesh/ready` and then `ready at <full sha>`, the order the forge counts (issue 913);
mesh-delivery merges it when its checks pass. 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 the forge's checks
operator's Approve of its merge is its review, and the review record says so.
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. No pull request, draft or not, exists before the review
approves the head, and no fix round is pushed to an open pull request: every push to an open pull request asks a new
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. A merge whose check has no verdict
(the checker drops a check still running at the merge) is reported to the operator as merged unchecked, never as
passed (playbook 07 step 3 item 2).
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>`,
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
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,
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
the mesh (the symptom gone, measured), and resolve the issue with `located-in` and `fixed-by` the same hour.
**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
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
(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
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
@@ -77,10 +145,10 @@ code.
|---|---|---|
| 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 ready mark | 15 minutes after the review approved | the reviewer marks it, or the lead does |
| the merge check | 15 minutes | read `mesh-controller.queue` and the check's log |
| the merge after ready | 5 minutes after the checks pass | 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 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 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 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.
@@ -92,10 +160,27 @@ reading itself and uses one subagent only for the review.
## How it is checked
- A small fix has one pull request with one or few commits naming its issue, opened after its review: a pull
request with many heads is a fix reviewed after its push.
- Its issue moves to `resolved` within the hour its delivery plan ends.
- A pull request reviewed and green but unmarked
for more than 15 minutes, a fix to a module's running marked ready with no integration test of the module whole, or a review report
asking a change outside the issue's done-when, is the failure of steps 4 to 6.
request with many heads is a fix reviewed after its pull request was opened.
- 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
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.
- A fixed code issue's note names the design line for each thing the fix adds, and its commit names the same lines
(`Builds:`); a fix adding what no design line gives, with no answered design question behind it, is step 1 skipped.
- A fixed code issue shows a note with `What exists today` (`issue-tracker.get`) older than its first commit, its
search written as run; one without is step 1 skipped, and a reader of a changed value met only after the delivery
is that note left short.
- A fixed code issue shows a note headed `Acceptance criteria` (`issue-tracker.get`) older than its first
commit, and each criterion's label is cited by a test in the pushed tree
(`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
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
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.
+535
View File
@@ -0,0 +1,535 @@
<!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 · then design complete</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">Build map · questions</text>
<text x="190" y="96" text-anchor="middle" class="mute">from the milestone part alone</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 design line, commit</text>
<text x="410" y="168" text-anchor="middle" class="mute">Builds: line · unit suite each 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">each change against its line</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="110" y="344" width="160" height="52" rx="8" fill="var(--card)" stroke="var(--op)" stroke-width="2.5"/>
<text x="190" y="366" text-anchor="middle" class="t">Answers each question</text>
<text x="190" y="384" text-anchor="middle" class="mute">written into the design</text>
<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 dash" d="M180,108 V342" marker-end="url(#ar2)"/>
<path class="edge" d="M270,370 C290,370 280,170 298,166" marker-end="url(#ar2)"/>
<text x="186" y="232" class="mute">questions</text>
<text x="276" y="262" class="mute">answered</text>
<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><span class="tag new">new</span>The milestone part of the design is the only input: the prepare maps each design line onto code (a build map per repository, each row citing its line) and lists every design question, what the design does not answer. It writes no findings or rulings of its own.</li>
<li><span class="tag new">new</span>Design complete: the operator answers each question, or the design writer one of internal form only (no change to what the operator sees, no permission, no data kept or removed), read by the reviewers by architectural domain; the answer is written into the milestone part, and only then does develop start. Nobody else answers one.</li>
<li><span class="tag new">new</span>What exists today, read from the code and the live mesh, never from memory: every effect of a part removed or moved (bus writes, files, credentials, plans, processes, calls answered), every reader relying on each, what happens to each after the change and its test. It opens each build map; no code before it is complete, and the review refuses work without it.</li>
<li><span class="tag new">new</span>Subtasks ordered so a user starts from the commit that adds its contract; one writer per file; one placement of the siblings for every agent.</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 and the design line it comes from.</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><span class="tag new">new</span>Build each design line as written and nothing it does not say; every commit names its lines (<code>Builds: design NN M §n</code>).</li>
<li><span class="tag new">new</span>A question the design does not answer stops that subtask and goes to the lead for the operator; never guessed or built around.</li>
<li>One commit per subtask, naming its issue; 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><span class="tag new">new</span>Integration tests compiled whole; once per head reported, those of the touched packages and their importers run against an embedded bus (by name where the importers are many); never a whole module's run.</li>
<li>Never push, open or merge; never move an issue; never ssh.</li>
<li>Code that differs from the build map: stop that subtask and report, file:line.</li>
<li><span class="tag new">new</span>Start a subtask only with room to finish it; stop between subtasks, never inside one.</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 whole, and once per head reported only those of the touched packages and their importers run there, against an embedded bus, never a whole module's or level's: 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">touched packages only</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 whole; touched packages run</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: a local run of the touched packages shows only that it passed.</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.","Write the numbered requirements from the answers, each with its check, before any option.","Reserve a number (kind research), never pick one by hand.","Propose the effort: Overview, Requirements, Questions, Method, sections named by title, no file references.","Research; findings in the section Findings, one version per settled set.","Try at the cheapest level that answers the question.","Write the Domains: modules, seats, glossary words added or retired.","Write the Direction: decisions, rejected options, acceptance tests.","Ask the operator to confirm the direction, one point at a time.","Review the direction by architectural domain: security, seams, running qualities, health, events and logs, testing, upgrade, tools.","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 · review by architectural domain 20 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 question the effort answers.","Propose one record per question: its options and the one chosen; design choices go in the design.","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: a main file and a part per milestone, leaving the builder no choice. (new)","A design question: the operator answers, written as a design line. (new)","Add the decisions, updated:, and the effort's glossary words.","Review the design by architectural domain before it is merged.","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.","Give each milestone its part: numbered design lines, at most 60 KB; the main file at most 80 KB. (new)","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, read the milestone part and the design's main file whole. (new)","Every agent is one of hq's definitions (mesh-preparer, mesh-developer, mesh-code-reviewer, mesh-watcher); the lead writes only the handover. (new)","The prepare's only output: a build map per repository, each row a design line and where it lands, and the design questions. No rulings. (new)","Design complete: the operator answers every question (the design writer one of internal form only), each written into the milestone part; then develop. (new)","Read what exists today from the code and the live mesh: every effect of what is removed or moved, every reader of each, what happens to each and its test. No code before it is complete; the review refuses work without it. (new)","The build map opens with what exists today; subtasks ordered contract first, one writer per file.","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 subtask, each citing its design lines; a question stops only its subtask. (new)","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 against the design lines, code no line asks for a finding, 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","A change built on what was remembered, not on what runs","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, the design lines it builds and the code; a design question goes to the operator (one of internal form only to the design writer) and into the design before code. (new)","Read what exists in the fix's area today, briefly: every reader of what the fix changes. (new)","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>
+3 -2
View File
@@ -14,7 +14,7 @@ and a forge address is an operational detail (see [`README`](../README.md)).
| Repository | Owns |
|---|---|
| `hal` | The monorepo — the node runtime, the module catalogue, the delivery machinery, and the bootstrap scripts. Every core module lives here. |
| `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. |
| *(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. |
@@ -36,6 +36,7 @@ one-for-one — `mesh-catalog`, `mesh-sdk` and `mesh-tools` exist where the tabl
| `mesh-surfaces` | 3 | tools, web, cli |
| `mesh-sdk` | — | the stable spine modules build against — the tool-serving SDK harness, the messaging/event framework, the contracts and core primitives. Holds nothing per-module and nothing volatile (ADR 0039). |
| `mesh-lab` | — | **exists.** The lab — scenario lifecycle, networking, placement. Ships to nobody; runs on a workstation. |
| `mesh-delivery` | — | the delivery domain: the parts that build, check and deliver a change by watching and writing keys on the bus, nine modules each whole in it, prefixed `mesh-delivery-` (ADR 0429). Made by the forge module from its config (ADR 0428). |
Tier 4's shape is open, and deliberately so: see ADR 0019 and
research 005.
@@ -47,7 +48,7 @@ Named by role, because the layout is itself part of the as-is design — see
| 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. |
| Bootstrap scripts | First-node initialisation, joining an existing mesh, and node rescue. |
| 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
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.
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
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
another. What makes them a mesh is a database that knows what should run where, and a message
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
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
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
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
status: implemented
code: [mesh-controller, mesh-catalog modules/mesh-delivery, mesh-catalog modules/gitea, mesh-catalog modules/build-agent]
updated: 2026-10-17
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-26
decisions:
- "ADR 0239"
- "ADR 0267"
- "ADR 0276"
- "ADR 0282"
- "ADR 0289"
- "ADR 0304"
@@ -17,74 +16,120 @@ decisions:
- "ADR 0382"
- "ADR 0395"
- "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
What runs on the mesh's main branches on 2026-10-16, read from the code and the live record by research 055
(findings 1 to 12). It is what [to-be 47](../01-to-be/47-delivery-from-commit-to-delivered.md) replaces, milestone
by milestone. Until 2026-10-16 this document described the predecessor's pipeline, which no longer runs.
What runs on 2026-10-26, after the delivery of release 6 (milestone R6 of
[to-be 47](../01-to-be/47-delivery-from-commit-to-delivered.md), ADR 0539), read from the code on each repository's
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
- **mesh-delivery** owns deliveries and delivery groups (ADR 0239): one delivery per commit, a group per branch
name shared across repositories. It records each transition in its state on the bus (key-value buckets
`deliveries` and `groups`), marks pull requests, asks the controller to deliver, and merges a pull request marked
ready at its head once both required checks pass against the trunk's head (ADR 0364) and the operator approved
the merge into main on a channel that proves who answered (ADR 0398 §3).
- **The controller** plans, orders, sends and judges. It computes a delivery plan for a repository and the files a
merge changes, moves only modules whose build source changed with their build dependents (ADR 0267), and
reuses a merge check's build of the same reuse key (ADR 0350).
- **The builders** (holders of the build seat) take build requests and checks from a work-queue stream on the bus
and build inside a toolchain image another module provides, with the network off; on 2026-10-13 no node had Go
or npm installed (issue 626).
- **The forge module** publishes every forge event on the bus through the forge's system webhook since 2026-10-14
(issue 729), and still polls the forge every 30 s, or every 300 s once it hears events. It keeps files of what it
announced so it does not announce twice.
- **The parts of `mesh-delivery`** are modules of one repository, each a process with its own bus credential that
watches keys and writes keys (ADR 0429). Running now: the forge bridge, the indexer, the checker, the teller and the
watchman on the control-node; the builder and the status view (`mesh-delivery-view`) on the workstation; and the
assigner on the home server. The gate (`mesh-delivery-gate`, holding the seat `mesh-delivery`) is a module of
`mesh-catalog` and runs on the control-node.
- **The forge bridge** turns the forge's events into stream messages and is the only part that calls the forge; the
forge module (`gitea`) still announces them through the forge's system webhook.
- **The indexer** writes each module's fingerprint at the merge preview and at a trunk merge; **the builder** claims a
build once per fingerprint and orders it as an ask on the stream of the artifact's language.
- **The build modules** (`mesh-build-go` on all four machines, `mesh-build-node` on three, `mesh-build-image` on one;
ADR 0525) hold the node seats `node-build-go`, `node-build-node` and `node-build-image` and pull asks from the bus's
work-queue streams (deliveries first, then checks, then re-checks) while their node has a free slot of build
capacity.
- **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
closes after 90 seconds without a merge or 10 minutes after the batch's first merge (ADR 0276). A lone merge is
cut at once (ADR 0282 §2, as ADR 0304 §1 changed it). The batch and its merges are rows in the controller's
store, cut by a timer every 5 s, with a catch-up timer every 5 minutes for merges no batch holds.
- **The rollout.** A closed batch becomes one rollout, at most one commit per repository, tiered along the module
graph. Rollouts run one at a time: a batch cut while a rollout is open is queued behind it, whatever it shares
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
the machines' reports or after fifteen minutes of silence (ADR 0360). A controller timer advances rollouts every
30 s.
- **A merge during a rollout.** Its place in the next batch is a store row the open rollout's holder writes. On
2026-10-16, when another controller worked the plans, a merge was not recorded and waited until a person released
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.
- A pull request's head is built and checked as it comes in; one status, `mesh/check`, carries the verdict, and a
delivery group has one verdict for every member. A move of the trunk asks the checks again, and a running check
stops itself when its head or trunk commit moves.
- The gate merges on the operator's Approve and moves the source revision in one write. Nothing collects merges or
waits for others: the indexer writes the new fingerprints, the builder claims each new one once, the controller
composes and grants on the done build, and the assigner writes each want once its rules hold, the canary first.
- Each node applies its own wants one module at a time and writes its haves; the source revision is delivered when
every want equals its module's fingerprint and every have equals its want, running and healthy. Read on
2026-10-26 at 01:20: 303 wants, every one equal to its node's have in fingerprint and composition digest, and 12
fingerprints held.
- **hq** builds no module: the checker runs hq's Go tests (`go test ./...`) as its check (ADR 0527).
## Where the record of what runs lives
Builds and build requests, rollouts with their tiers, batches and merges, and assignments are rows in the
controller's store; artifacts are blobs in the artifact store by digest (ADR 0251); packages are versions in the
package registry. The controller writes three record mirrors to the bus: assignments, declared config and
predefined keys; of the five ADR 0289 §7 decided, the nodes, rollouts, builds and settings mirrors are not in the
code. An assignment names its module and node but not the build, artifact or rollout that put it there: which commit
runs where, and which delivery brought it, takes three verbs and a join by hand.
No backup is taken before a delivery moves a module that holds data; backups run nightly and on demand (ADR 0214).
Fingerprints, builds, verdicts, wants and haves are keys in the parts' buckets on the bus; compositions are in each
node's `composed-<node>` bucket, readable only by that node, with a file over 64 KiB in the object store; builds'
files are in the artifact store by digest; packages are versions in the package registry. The controller
still records every build it takes in, in its builds bucket (`RecordBuild` in `cmd/mesh-controller/key_takein.go`); its
assignments bucket keeps placement alone, and its rollouts bucket keeps the record of releases 1 to 5 read-only, and
nothing writes it now.
## 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,
and the lab is not used (the operator, 2026-10-16).
Nothing checks the delivery budget: the over-budget condition and the gate's `times` went with release 6, and no
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
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.
## 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.
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
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:
- **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.
- **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.
+8 -8
View File
@@ -9,9 +9,9 @@ decisions:
- "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
(ADR 0001).
@@ -30,7 +30,7 @@ By namespace, and the namespace records origin rather than purpose:
## 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 |
|---|---|
@@ -51,7 +51,7 @@ connectivity is made four times.
## 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
offers.
@@ -64,7 +64,7 @@ deliberately does not yet settle the domain list.
## 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
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
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
inheritance. See ADR 0012, whose case
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
(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.
## Known inconsistencies in the catalogue itself
## Known inconsistencies in the catalog itself
Recorded because a reader will meet them:
+2 -2
View File
@@ -11,7 +11,7 @@ decisions:
# 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
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
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
private network, the resolver's configuration and the showcase — mark a role without answering for
anything a consumer requires.
+131 -47
View File
@@ -1,69 +1,153 @@
---
layer: as-is
status: implemented
code: [mesh-catalog modules/mesh-console, mesh-tools src/mesh.ts, mesh-tools src/http.ts, mesh-tools src/runtime.ts, mesh-controller internal/broker, mesh-controller cmd/mesh-controller/check.go, mesh-controller cmd/mesh-controller/seatverbs.go]
updated: 2026-10-10
code: [mesh-tools, mesh-host, mesh-controller]
updated: 2026-10-26
decisions:
- "ADR 0539"
- "ADR 0360"
- "ADR 0524"
- "ADR 0525"
- "ADR 0152"
- "ADR 0095"
- "ADR 0037"
- "ADR 0175"
- "ADR 0195"
- "ADR 0197"
- "ADR 0367"
- "ADR 0154"
---
# The mesh MCP server, as it runs
**The mesh's tools reach a person through a module the mesh assigned to their machine.** Since
2026-09-30 a workstation that is a node can be assigned `mesh-console`; the mesh mints a bus account
`<node>.mesh-console`, seals its credential to the machine, and the container binds
`127.0.0.1:<port>` with the port the mesh assigned for the manifest's declared one. An agent on the
machine is pointed at `http://127.0.0.1:<port>/mcp` and sees the mesh's tools; a person uses the same
endpoint. Nothing on the machine holds a credential a person had to carry.
**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
through `mesh_overview`, `mesh_machine`, `mesh_runtimes` and `mesh_describe`. Checked: the
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
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
**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'
check treats the four the same way, as the core whose health the mesh itself defines rather than a
module's own. Since release 6 it reaches each node the way every module does: the controller composes
and signs its composition for that node, the assigner writes the node's want of it, and the
node-engine's node agent applies it and writes its have. What stays particular to it is the account
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
where its grants live: `invokes` names `*`, so its account may call every tool on the mesh;
`waits` names the five event families `mesh_wait` may subscribe to (the knowledge base's reviews,
mesh-delivery's transitions, the forge's status, the controller's checked event, and the operator
channel's decision for the agent module) — the bus refuses a subscription outside this list
(ADR 0367 §2); and it
`provides` one provision, the MCP endpoint, at node scope, and `listens` on the port it serves from,
opened only to the machine itself. Its code lives under `node-tools/` in the `mesh-tools`
repository (`internal/console`, `internal/announce`, `internal/bus`).
**One runtime per node, run by the node-engine as a sibling process, never a container**
(ADR 0175). It holds the node's one bus credential and may call every tool on the mesh; its account
publishes nothing else. It binds `127.0.0.1:<port>` and refuses to bind anything else, said before
binding rather than after (`node-tools/internal/console/http.go`, citing ADR 0152): whoever is on
the machine owns the mesh there, and nobody else may reach this port. An agent or a person on the
node is pointed at `http://127.0.0.1:<port>/mcp` once; the `serverInfo.name` the handshake answers
is `mesh`.
## What it answers
`initialize`, `tools/list`, `tools/call`, over HTTP, one JSON body per request, no session and no event
stream. `tools/list` is what the running modules answered: every tool runner built on or after that day
serves a `tools` verb for its module, and the mesh MCP server asks the catalogue for the roster and each module
for its tools. A module that did not answer is named in the list's `_meta.notAnswering`. On the day it
shipped that was 36 of 51 modules — those that serve no tools at all, and those whose rebuilt runtime the
mesh records rather than rolls out — and 62 tools from the rest.
`initialize`, `notifications/initialized`, `ping`, `tools/list`, `tools/call`, over HTTP, one JSON
body per request, no session and no event stream (`node-tools/internal/console/mcp.go`).
`tools/call` reaches any tool by `<module>.<tool>`, listed or not. The mesh MCP server's grant is `*`, so what it
may call is every tool on the mesh; its account may publish nothing else and subscribes nothing.
**It announces seven tools, not five** (ADR 0195, extended by ADR 0367): `mesh_overview`,
`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
where the mesh-wide count of modules and tools is read live — a number copied into this document
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
absent.
No call is refused for want of a person's approval: a tool runner serves every call it is issued, whoever made
it, and cannot tell an agent from the operator (ADR 0259 §6, issue 340). An act waits for the operator's warrant
only where its module asks first, in its own code: the agent module's instruction proposals, and the
controller's settings proposals and the actions it offers on a condition.
**Addresses, exactly as the handshake states them:** `<seat>.<verb>` for a seat held once for the
mesh (`mesh-controller.status`); `<node>/<seat>.<verb>` for a seat every machine holds
(`<node>/node-packet-filter.rules`); `<node>/<module>.<tool>` for a module assigned to one machine;
and `<module>.<tool>` also for a module whose instances are interchangeable. A tool not listed can
still be called by address — listing is discovery, calling is the grant — and a module that is not
interchangeable is refused with the machines it runs on rather than guessed at.
**What exists is what announced itself** (ADR 0197). Every node's tool runner answers the NATS services
protocol's `$SRV.PING`, `$SRV.INFO` and `$SRV.STATS`, one service per process, naming in each
endpoint's metadata which module, seat, scope and machine serves it. `mesh_overview` and
`mesh_machine` gather one request's answers: at least 750ms so a runtime that only just answered
`$SRV.PING` is still waited for, up to 5s for one that said it is there but has not yet sent its
full announcement — a fixed shorter window lost the largest runtime's listing once a live
measurement found how slow its round trip over the bus could be. An announcement unchanged since
last asked (by its digest) is not re-fetched, which is why `mesh_runtimes` can show a runtime
answering in under 100ms for hundreds of kilobytes of discovery.
## mesh_wait
**The seventh tool, added by ADR 0367**, lets a session wait on the bus for an event instead of
polling: a call names one or more granted event families and an optional filter, and either answers
the first match or says it timed out. The tool runner's own bus account hears only the families its
manifest's `waits` list names; an event it hears is never taken as the state — a session reads the
state from the verb that owns it once woken. It is meant to be called in a loop from a background
command, not inline, because one call blocks until it answers or times out.
## The mesh's own verbs
*Since 2026-09-30 evening (ADR 0154).*
The mesh MCP server asks the `mesh-controller` seat's `tools` verb beside the modules and lists every role's
tools as `<seat>.<verb>` — `mesh-controller.status`, `mesh-controller.push` and the other ten. A call
to `<prefix>.<name>` reaches the seat when the prefix is a seat declaring that verb, and the module
otherwise; `seat:<seat>.<verb>` says so outright. When the controller does not answer, the list
names `mesh-controller (seat)` as not answering and carries the modules' tools regardless. The
`mesh-controller` *module* is always named as not answering: it serves no module tools, only its seat's.
**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
live on 2026-10-26, the mesh-controller runtime is one instance, mesh-wide, and it answers 65 verbs for
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
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
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
mesh-controller runtime itself, a separate process, and the tool runner only asks it and relays what
it answers.
## Around it
## Where it runs
- **`invokes`** in a manifest is the grant. It is composed into the bus's user list exactly as a
person's account is; the mesh MCP server is the only module that declares it.
- **`module check <file|dir>…`** on the controller's binary judges a manifest with no mesh: the strict
parse, every per-manifest problem, and the rules between the manifests given. It prints what it cannot
judge without a store rather than refusing. The mesh MCP server's own manifest was the first thing checked
with it, and the whole catalogue passes.
- **The person's client remains.** `operator issue` and `mesh tools|call|mcp` with a credential file
still work, for a machine that is not a node and for a mesh not yet able to assign anything.
`mesh tools --console <url>` goes through a running mesh MCP server with no credential; it is covered by the
runtime repository's tests and was not exercised on the live mesh.
**Every node, not a per-operator assignment.** Measured live: all four of the mesh's machines
answer a `node-tools` runtime, each with its own module and tool count (between 294 and 312 tools,
56 to 93 seat verbs, in this reading). This is a fact that shipped past the design it came from: the
to-be document still describes it as assigned to "whichever machines an operator sits at"; what
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
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 catalog exposes.
## What shipped bent
## What this repository's prior text got wrong
- A module registered by hand from the catalogue with `--source <url> --path modules/<m>` records a URL,
not a place on the git seat: `--self` takes the forge path form (`<owner>/<repository>`), which the
operator did not pass. The rebuild-on-merge matched the URL anyway.
- Modules whose upgrade policy is *record* — the forge among them — answered `tools` only once
something pushed their rebuilt runtime; until then they are listed as not answering while still
callable. That is the policy doing what it says, not a fault of the mesh MCP server.
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
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
manifest's `build.artifacts` names a Go binary built from `cmd/node-tools`, and that is what a node
runs. The repository still carries the earlier TypeScript sources (`node-tools/src/mesh.ts`,
`http.ts`, `runtime.ts`, `announce.ts`, `broker-nats.ts`, `client.ts`, `launch.ts`, `main.ts`,
`mcp.ts`) on main, but the manifest does not build from them. The quirks the prior text recorded
from the `mesh-console` era (a module registered by hand recording its source as a URL; a
*record*-policy module staying listed as not-answering until its rebuilt runtime is pushed) are not
re-checked here: they were about the `mesh-console` container's registration path, which no longer
exists.
## How it is checked
- `mesh_overview` and `mesh_machine <node>`: the live tool and seat-verb counts, and which modules
are named not-answering.
- `mesh_runtimes`: which runtimes answered discovery, how long they took, how large their
announcement was, and whether it was shortened to fit the bus — read this when an address is said
to be missing or on another machine.
- `mesh_describe <address>` for one tool's schema; a call through `mesh_call` reaches the module or
seat over the bus and the answer is its own, unshaped.
- The code: `mesh-tools` (`node-tools/internal/console`, `internal/announce`, `internal/bus`),
`mesh-host` and `mesh-controller`'s core-component handling of `node-tools` (witness, lease,
bus-user naming). Their unit tests hold the loopback-only refusal, the seven-tool handshake, the
discovery gather window and `mesh_wait`'s event-then-read-state rule.
- `knowledge-base.get` with `kind: decision` and number 152, 175, 195, 197, 367 or 154.
+3 -2
View File
@@ -5,6 +5,7 @@ code: [mesh-controller internal/catalogue/state.go, mesh-controller internal/bro
updated: 2026-10-04
decisions:
- "ADR 0201"
- "ADR 0439"
---
# A module's state, as it runs
@@ -17,7 +18,7 @@ machine and its licence manager; on the day this was written, three buckets exis
## What runs
- **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.
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
@@ -45,7 +46,7 @@ machine and its licence manager; on the day this was written, three buckets exis
## 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,
the TypeScript SDK through the runtime); `module check` names a read whose owner on the shelf keeps no
such state.
+13 -1
View File
@@ -2,9 +2,11 @@
layer: as-is
status: implemented
code: [mesh-controller, mesh-tools]
updated: 2026-10-16
updated: 2026-10-19
decisions:
- "ADR 0106"
- "ADR 0436"
- "ADR 0438"
---
# 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
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
- **No expiry.** A login lives until its record is deleted and the list rewritten on the bus's node.
@@ -0,0 +1,116 @@
---
layer: as-is
status: implemented
code: [mesh-controller, mesh-catalog modules/claude-code, mesh-catalog modules/gitea, mesh-catalog modules/ssh-client]
updated: 2026-10-17
decisions:
- "ADR 0266"
- "ADR 0399"
- "ADR 0409"
- "ADR 0410"
---
# Accounts on a machine, as they run
**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
login, and a module that needs one declares it itself.** Read on 2026-10-17 from the controller's and the
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
replaces this document when it is built.
## The node record
- **The operator account is stated at the controller's terminal** with `node account <node> <account> [home]`.
It is the person's login on that machine. The controller refuses to set it to the node's agent account.
- **The agent account is stated at the controller's terminal only** with `node agent-account <node> <account>
[home]`, and cleared with `--clear`. It is no verb and no setting, so an agent cannot name itself one
(ADR 0266 §1). The controller refuses root, a malformed login, a well-known service account and the
operator account.
- **Five machine facts come from the record.** `account` and `account-home` are the operator's, absent when
the node has no operator account. `agent-account` is the agent account, or the operator account where none is
named; `agent-home` is its home (`/home/<account>` when no home is stated); `agent-root` is `never` only where
the node names an agent account of its own, otherwise empty. Modules read them as `${machine:...}` words.
- **One node of four names an agent account today**, the control node. The workstation, the laptop and the
home server name none: `mesh-controller.node` says there "agents run as the operator account; no agent
account is named", and the plan for the workstation declares the `claude-code` account resource with the
operator's name and no `root` field. The tool runner also receives `MESH_AGENT_ACCOUNT` and `MESH_AGENT_HOME`.
- **Not verified:** whether the mesh-config snapshot carries either account. The controller's code reads both
from its own node table (columns for account, account home, agent account and agent home); no second place
that states them was found.
## What claude-code does with the agent account
- **It declares a `user` resource** named `${machine:agent-account}` with home `${machine:agent-home}` and
`root: ${machine:agent-root}`, and a `~/.claude` directory under that home owned by it, mode 0700. On the
control node the plan holds the resource with `root: never`; elsewhere it holds the operator's account.
Where the account is new, the mesh creates it with a home, its own group and no password (ADR 0266 §2).
- **`claude-agent`** is a file the module writes at `/usr/local/bin/claude-agent`. Run as the agent account
it starts `claude`; run as any other account it runs `sudo -iu <agent account> claude` through that
account's own sudo, carrying the operator's guard override only if it is set in that shell. The file
`/etc/claude-code/agent-account` holds the account's name.
- **The guard of ADR 0266 §28** is the module's hook on every tool call. On a node with an agent account
of its own it refuses each call of a session that runs as another account, with no override, and tells the
session to start with `claude-agent`. Its record goes to the machine's journal. The same guard also refuses
ssh to a mesh machine and writes to `/etc/hosts` and the resolver file (ADR 0245).
- **Not verified live:** this rests on the guard's code and README, not on a refusal seen on a running machine.
## The judgement that the agent account cannot become root
- **The node-engine judges `root: never` on every look**, by reading only: uid 0, a group that grants root,
a sudo, doas or polkit rule, a container runtime's socket or API port, a secret the mesh placed for another
account, a setuid program no package owns, and kernel link protection off (ADR 0266 §12 to §16). A way found,
or a question unanswered, makes the account resource unhealthy. It does not prevent any way to root.
- **The controller's self-check holds** that a node naming an agent account has it judged, in a verdict heard
within 15 minutes, as unable to become root (ADR 0266 §21 to §22). Otherwise it raises the urgent condition
`agent-can-become-root`. `mesh-controller.node` shows the verdict: for the control node it reads "cannot
become root without a person", judged the same day.
- **On the three nodes without an agent account** the check does not apply. The controller instead holds an
urgent condition `root-not-free` on each machine that runs reads or the mail notifier. Two of the three are
silenced until 2026-10-23 on the operator's decision; the home server's is open and not silenced.
## The accounts agents push as on the forge
- **Today the forge has a user per machine**, made by the gitea module's `forge-account` provision and
holding that machine's key, plus a write token the module still mints for each (ADR 0398, ADR 0400). ADR 0410
decides one forge user, `nox-agent`, in the agents' write team, each machine's key titled by machine. No tool
reads the forge's user list, so whether `nox-agent` exists yet is **not verified**.
- **The forge key is meant for the agent account** (ADR 0409 §2), but a module cannot run as that account:
the controller refuses it (issue 989, open). So the `ssh-client` module's key keeper still runs as the
operator account.
- **An ordering deadlock** between `ssh-client`, which wants `forge-account`, and `gitea`, which provides it,
stopped every node's declaration from composing on 2026-10-17 at 00:35 until gitea was registered three
minutes later (issue 986). The four urgent conditions it raised were not observed again but stay raised,
because the controller's declaration check times out and never clears them (issue 992).
Git over ssh was proven from the laptop only; the proof on the other three, and retiring the forge token the
module still places, wait on issues 989 and 991 (the latter in `fixing`).
- **Main stays closed**: a push to the main branch is refused, and a merge waits for a person (ADR 0398).
## Logins on the bus
Not here: see [`16-logins-on-the-bus.md`](16-logins-on-the-bus.md).
## What it cannot do
- **No identity carries a login.** An agent, a person and a module are not tied to an account on a machine;
the node record names a login per node and nothing else.
- **A module declares a login itself.** Each module that needs an account writes a `user` resource of its own,
and nothing checks that two modules agree or that a login exists on every machine that needs it.
- **Agent and operator share a login on three of four machines.** There the agent runs as the operator account,
which may become root; the guard's account rule does not apply, and the exposure is a held condition, not a fix.
- **No per-agent bus password.** Every session on a machine calls through that machine's tool-runner login
(document 16), so one agent's calls cannot be told from another's.
- **Detects, does not prevent.** The judgement above lists the ways to root it looks for; a way it does not list
is not seen (ADR 0266 §17).
## How it is checked
- `mesh-controller.node` with a node name: "agents run as" line, the verdict and when it was judged, the open
conditions (`agent-can-become-root`, `root-not-free`, `uncomposable`).
- `mesh-controller.plan` with a node name: the `claude-code.agent-account` resource and its `root` field; with
`files` set, the files given, including `claude-agent` and `agent-account`.
- `mesh-controller.conditions` for the held conditions; `claude_code_guard` for the guard's record, `claude-code`
tools for its status.
- `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
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).
+3 -1
View File
@@ -18,13 +18,15 @@ 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 |
| [`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 |
| [`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 |
| [`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 |
| [`14-a-modules-state.md`](14-a-modules-state.md) | A module's current state on the bus: what the controller creates, the runtime serves, and the first live use showed |
| [`15-the-agent-and-its-licences.md`](15-the-agent-and-its-licences.md) | The operator's agent on every machine and the licence manager that keeps its licences |
| [`16-logins-on-the-bus.md`](16-logins-on-the-bus.md) | The bus's one list of logins, a login per module assignment, persons limited to their tools, sessions calling through the tool runner's login, and no effect on any tool |
| [`17-accounts-on-a-machine.md`](17-accounts-on-a-machine.md) | The operator and agent accounts on the node record, claude-code's own login and guard, the judgement that the agent account cannot become root, and the forge accounts agents push as |
| [`18-the-mail-modules.md`](18-the-mail-modules.md) | mesh-mail's accounts and MAIL stream, the one `grants` list that gives both reading and sending at once, the send path through its own process and the operator's Approve, mesh-mail-web's pages and sign-in, and mesh-mail-notifier's pop-up and its quiet rules |
## What these documents are not
+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
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);
decided in ADR 0089.*
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
when each bed was written; the copies did not move when the catalogue did, and a catalogue change
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 catalog did, and a catalog change
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
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.
@@ -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
module's name is its tool namespace and its broker scope
(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.
**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.
*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
a declaration for a copy that is gone; the receipt test asserts the catalogue is claimed whenever
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 catalog is claimed whenever
the run is pointed at one.
## 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
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
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
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
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
(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,
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
ADR 0141 describes. Third-party
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
(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
whose program would otherwise overwrite it. Each of the catalogue's managers' modules
(NetworkManager, systemd-networkd; dhcpcd's left the catalogue with
whose program would otherwise overwrite it. Each of the catalog's managers' modules
(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
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
@@ -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
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
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.*
**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
*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
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
@@ -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
(ADR 0099).
*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**
(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.
*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
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.*
## 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
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
test that used to be the only check, `TestEveryCatalogueManifestParses`, now stands beside a command
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
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
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.*
@@ -398,7 +398,7 @@ that distinction once already.*
## The four the loop cannot build
*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
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 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 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
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
ADR 0070: where the init builder
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.
**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
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
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
*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
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.
@@ -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`
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
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`
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.
**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
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.
+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
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.
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
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
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
@@ -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
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
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
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.
**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
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
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.
**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. |
| 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 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. |
| 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. |
+14 -12
View File
@@ -1,11 +1,11 @@
---
layer: to-be
status: proposed
status: in-progress
code:
- mesh-controller cmd/mesh-builder
- mesh-controller internal/builder
- mesh-catalog modules/build-agent
updated: 2026-10-16
- mesh-controller modules/build-agent
updated: 2026-10-17
decisions:
- "ADR 0405"
- "ADR 0262"
@@ -31,6 +31,8 @@ decisions:
# Building a module
**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 builder (internal/builder, cmd/mesh-builder) implements the recipe kinds described here, and the sections marked built and proven live match the code. Left: not broken down into milestones; issue 1072 found it saying proposed.
[`12-a-module-repository`](12-a-module-repository.md) says what a module may build — an image, an
archive, an upstream mirror — and where the result goes. This says **how a build is modelled**, and
why the current model does not fit what a module is.
@@ -41,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.
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.
## The language
@@ -81,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
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
example sitting open in the next window.
@@ -161,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. |
| 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. |
| Nothing is announced that was not made | A build made to fail announces nothing, and the catalogue'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. |
| 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 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 whole surface, in one place
@@ -205,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`
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
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
@@ -245,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)) | ✅ |
| `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 | ✅ |
| `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 | ✅ |
| `package` | a package that must be present | ✅ |
| `network` | a named container network | ✅ |
@@ -290,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.
*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
([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)),
@@ -314,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
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
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
builder needs nothing more for it.
@@ -381,7 +383,7 @@ than retried for ever. Live: the store's size before and after the first nightly
*2026-09-29 —
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
an image from a hand-written Dockerfile, which is the per-repository incantation this whole mechanism
exists to abolish.
+4 -2
View File
@@ -1,13 +1,13 @@
---
layer: to-be
status: proposed
status: in-progress
code:
- mesh-sdk src
- mesh-tools src/broker-nats.ts (and broker-amqp.ts until the rollout)
- mesh-controller internal/link
- mesh-controller cmd/mesh-controller (status)
- mesh-catalog modules/postgres, modules/keycloak (the Go provisioner loop)
updated: 2026-10-06
updated: 2026-10-17
decisions:
- "ADR 0224"
- "ADR 0230"
@@ -24,6 +24,8 @@ decisions:
# The module protocol
**Where it stands, 2026-10-17** (the records cleanup, issue 1072; playbook 07, "The design's state is written back"). Built on main: the wire this design describes is in mesh-sdk and mesh-controller's internal/link, and the design's own checks against the code (2026-09-16) agree. Left: not broken down into milestones; issue 1072 found it saying proposed.
**What a module's code and the mesh say to each other.** An SDK is an implementation of this in one
language and nothing more (ADR 0074).
+44 -3
View File
@@ -5,8 +5,17 @@ code:
- mesh-catalog modules/showcase
- mesh-controller internal/builder
- mesh-sdk src
updated: 2026-10-02
- mesh-contracts
updated: 2026-11-05
decisions:
- "ADR 0634"
- "ADR 0635"
- "ADR 0625"
- "ADR 0627"
- "ADR 0573"
- "ADR 0606"
- "ADR 0609"
- "ADR 0593"
- "ADR 0150"
- "ADR 0099"
- "ADR 0053"
@@ -47,10 +56,42 @@ Split the way the protocol is split:
mesh set. Reading a file is not a thing an SDK needs to teach.
- **No API clients.** A Plex client belongs in the Plex module. It changes when Plex changes,
which has nothing to do with any other module — *frequent **and** cascading is the disease.*
Logic over a *seat* is not such a client: it is the seat's client package, in the seat's contracts
module (below), never in the SDK and never in a holder.
- **No storage, no HTTP framework, no logging library.** Use the language's.
If you find yourself wanting to add something to the SDK, the test is ADR 0039's: **does editing it
recompile unrelated modules, and does it change often?** Both, and it does not belong.
recompile unrelated modules, and does it change often?** Both, and it does not belong. It goes to
its owner instead: a seat's contract or client, the module that owns it, or its domain's SDK (*Where a seat's code
goes*, below; ADR 0634 §1).
## Where a seat's code goes
*Added 2026-11-04, widened 2026-11-05* (ADR 0625, ADR 0627, ADR 0634, ADR 0635; [design 26](26-the-seats.md) *Where
a seat's code goes* is the whole rule). Shared code lives with its owner (ADR 0634 §1): a module that calls or holds
a seat, or shares code with other modules, meets these places, and each piece of code has one, the first that fits:
| the code | lives in | written |
|---|---|---|
| a seat's types, subjects, the layouts of its states and a typed call per verb or accept | the seat's **contract**: a package of its own per language in its domain's contracts module (`mesh-<domain>-contracts`, or `mesh-contracts` for the Core domain and a domain with none), for a seat defined whole; a seat at the seat list's grain has only its definition, which every reader reads from the bus (ADR 0635 §1, §2) | generated from the seat's definition, never by hand |
| logic two or more callers or holders would otherwise each write over that seat (an ask and its awaited answer, publishing a file in parts to a registry) | the seat's **client package**, beside its contract, owned by the contracts module | by hand; it uses only its own seat's contract, mesh-sdk's runtime and its own domain's SDK, never another seat's client, and names no holder, no module's tool and no module's subject |
| logic and state layouts one module owns, where it holds no seat for them | that module's **own package**, which other modules import from it: by path in the same repository (ADR 0609 §2), by a published version from another (ADR 0573 §1), so its manifest declares it as a package artifact; its fingerprint counts only that package's files (ADR 0593 §2), so its importers rebuild only when it changes | by hand, in the owner module |
| logic and forms no seat carries and no one module owns, used by one domain's modules or seats only (moving a file in parts, the build steps' shared logic) | that domain's **domain SDK**, `mesh-<domain>-sdk`, a package-only module (ADR 0634 §2) | by hand; it imports only mesh-sdk and contract packages, names no module's tool or subject, and holds no seat's type or state layout |
| what every module needs whatever the seat (the bus connection, the tools bundle's runtime, state), and a part modules or seats of more than one domain share that names no seat, verb or seat's type | **mesh-sdk** | by hand; it takes no seat-specific type, imports no contracts module, and reads the seat list from the bus |
| the seat's implementation | the **holder** module | by hand, against the contract; never the place of the contract or a client |
**So a module imports the contracts and clients of the seats it uses, the packages of the modules whose state it
reads, its domain's SDK where it uses it, and mesh-sdk.** Its users rebuild when something they use changes, not when
any seat or any domain does. A seat you define yourself, outside the mesh's own contracts, gets a
contracts module of its own, which may share your holder module's repository (ADR 0625 §7): a second holder then
imports only the contract, never your holder. A contracts module and a domain SDK are modules like any other, found by
their manifests in folders of a repository and delivered to no node (ADR 0634 §8). And a role no other module could
ever take over is no seat at all: it is your module's own tools and events (ADR 0625 §8).
*How it is checked:* by the checks design 26 lists: the generators' tests, mesh-sdk's import and package-list tests,
the build modules' regenerate-and-compare step, the catalog check of a client and the manifest validator's refusal of a
contracts module that claims a seat; the home of each new exported name, a domain SDK's importers included, and a role
made a seat or a seat left as one module's tools, by the review of the pull request that adds it (ADR 0634 §10).
## A module with four capabilities, in four languages
@@ -171,7 +212,7 @@ knows. Putting the Plex client anywhere else is the shared-library disease with
- **Which private registry.** That there *is* one is settled —
ADR 0014 says each module consumes its dependencies
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
nothing has installed one when the first SDK is built. That is the same pivot as everything else
and it has not been designed.
@@ -1,11 +1,11 @@
---
layer: to-be
status: proposed
status: in-progress
code:
- mesh-host internal/bootstrap
- mesh-host cmd/mesh-bootstrap
- mesh-lab test/integration/one-node-mesh.test.ts
updated: 2026-10-06
updated: 2026-10-17
decisions:
- "ADR 0226"
- "ADR 0067"
@@ -16,6 +16,8 @@ decisions:
# The installation, in full
**Where it stands, 2026-10-17** (the records cleanup, issue 1072; playbook 07, "The design's state is written back"). Built on main: the node-engine's bootstrap (`mesh-host` internal/bootstrap) carries phases 2 and 3 of the installation, so the steps this text calls manual are partly automated. Left: not broken down into milestones; the text is not yet corrected against the code.
Every step from a machine with nothing to a mesh that maintains itself. Written out explicitly
because it is the procedure everything else depends on, and because the parts that do not exist
yet are easier to see beside the parts that do.
@@ -63,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
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 |
|---|---|---|---|
| 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 |
| 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 |
| 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)) |
| 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 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 |
| 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 |
+5 -3
View File
@@ -6,7 +6,7 @@ code:
- mesh-controller
- mesh-catalog
- mesh-lab
updated: 2026-09-15
updated: 2026-10-17
decisions:
- "ADR 0067"
- "ADR 0074"
@@ -17,6 +17,8 @@ decisions:
# The work ahead
**Where it stands, 2026-10-17** (the records cleanup, issue 1072; playbook 07, "The design's state is written back"). Phase 2's blocker (issue 053) is resolved and delivered, and the forge's module declares the package registry in full, Phase 2's done-when. Whether every phase holds is checked under issue 1073, which marks it implemented or names what is left.
Everything decided this cycle and not yet built, in the order its dependencies allow. Each phase
ends at something provable on a running mesh, because a phase that ends at a claim is a phase that
went missing without anything complaining.
@@ -42,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
(`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
required headers). The dead types that disagreed are removed, ADR 0074 and doc 19 corrected
@@ -89,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
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 —
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
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.)*
+5 -3
View File
@@ -1,8 +1,8 @@
---
layer: to-be
status: designed
code: []
updated: 2026-09-25
status: in-progress
code: [mesh-controller]
updated: 2026-10-17
decisions:
- "ADR 0084"
- "ADR 0027"
@@ -11,6 +11,8 @@ decisions:
# 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.
A provision is named for what the consumer's code is coupled to
(ADR 0027):
`postgres-database`, not `database`. That decides *what kind* of provider satisfies a requirement.
+64 -10
View File
@@ -1,9 +1,10 @@
---
layer: to-be
status: implemented
code: [mesh-catalog, mesh-controller, mesh-host, mesh-media-catalog]
updated: 2026-10-12
status: in-progress
code: [mesh-catalog, mesh-controller, mesh-host, mesh-media-catalog, mesh-vault]
updated: 2026-10-26
decisions:
- "ADR 0550"
- "ADR 0305"
- "ADR 0158"
- "ADR 0094"
@@ -31,9 +32,10 @@ wrong.
- **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
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
already has an owner and a rotation
([13](13-credentials-and-their-rotation.md)); disturbing it would buy nothing.
and its provider makes it true. This design first left it out of the vault, since it already has an owner
and a rotation ([13](13-credentials-and-their-rotation.md)). **It is the vault's now** (ADR 0550 §5, from
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
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.**
@@ -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
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 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
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
ordinary way, and it does **not** mint the mesh's delivery credentials — the controller does that,
because the controller must mint in order to deliver any provision, the vault's own included.
ordinary way. It does not yet mint the mesh's bus accounts: until the third milestone of ADR 0547 the
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.
Node scoping (ADR 0084,
@@ -131,7 +185,7 @@ recovery addresses both alike.
### 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 —
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
*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
keeps the token the vendor issued. The mesh can neither make nor rotate such a value, and a copy typed for
@@ -0,0 +1,181 @@
---
layer: to-be
design: 25
milestone: bindings-first-view
updated: 2026-11-11
code: [mesh-contracts seats/mesh-broker, mesh-sdk go/seatlist, mesh-catalog modules/nats, mesh-delivery modules/mesh-grants]
decisions:
- "ADR 0595"
- "ADR 0615"
- "ADR 0621"
- "ADR 0596"
---
# Design 25, milestone bindings-first-view — the grants part asks the bus module for every binding
**What it builds.** The grants part writes a module's binding (the names on the bus of its bus-state entries) into
its membership when it hears the bus module's `created`, rewrites it on `recreated` and removes it on `dropped`
(design 25 bus-state §4; ADR 0595 §1, §7). Until now those live events were the binding's only source, so an event
the grants part never heard lost the binding for good: on 2026-11-11 a registry holder's membership lacked three of
the entries its manifest asks for, although the bus held all three, and about 190 publishes since 2026-11-09 were
never said `published` (issue 1966). This milestone gives the grants part a first view of every binding from the
owner of that data, the seat `mesh-broker`, at every start and when asked again by hand, so a missed event is
recovered with nobody's hand. The live events stay the way a binding changes between starts.
**Where it comes from.** The operator, 2026-11-11 (about 03:35 CET, in the terminal), chose "Ask the bus module":
at every start, and on a re-ask, the grants part takes a first view from the bus module's seat of the entries it
holds and composes the bindings from it; the owner answers for its own data. Within that answer, these lines follow
ADR 0595 §1 and §7 (the binding is the bus module's to say and the grants part's to write), ADR 0596 §2 (another
module's data is asked through its seat, never read from its bus state) and the first-view rules of ADR 0615 §3 to
§6, which decide them for the indexer's and the assigner's state and which this milestone applies, as the design's
choice, to the bus module's. Each choice below says so. The operator, 2026-11-11 13:30 CET (in the terminal, to lead-47), asked whether
the grants part should take a bucket's binding from the bus module's existing `list` answer when the new field
`binding` is missing, to break the circle §7 names, answered "Yes, build it": §9.
**Cited as** "design 25 bindings-first-view §n". It amends design 25 bus-state §4, which says so.
## The lines
- §1 **The first view is the seat `mesh-broker`'s `list`, asked with no `module`.** No new verb: `list` already
answers every entry the bus module created, from its own state `asked`, by module and local name, and lacks only
the binding (§2); a second verb answering the same entries would be a second form of one fact. The grants part
declares `seat:mesh-broker.list` in its manifest's `invokes`, and composes its own grant of it as it composes any
invoker's. `list` names no class, so from milestone G of design 47 it is a `read` and takes that class's callers,
invokers, persons and sessions (design 47 G §391); until then its callers are "any caller whose role grants it",
which the grants part's invoke grant meets. The verb stays answered where the bus module's tools answer it today,
by its tools bundle in the tool runner of the machine holding the seat, and gains no runtime of its own: ADR 0621
§1 gives the indexer's and the assigner's first views a runtime of their own so that the tool runner never asks
through itself, and here the asker is the grants part, which composes every account without this answer (§5), so
no part waits on itself. Why: the design's choice under the operator's answer and ADR 0596 §2. How it is checked:
§8, the whole case.
- §2 **Each entry of `list`'s answer carries its binding.** The seat's `Held` gains the field `binding`, of the
seat's own form `Binding`, the same value the bus module's `created` and `recreated` carry for that entry in their
field `binding`, taken from the entry's plan in the bus module's state `asked`. **`Binding` is reshaped to the form
on the wire.** Today the seat's definition says `Binding` is a bus name, a storage type, a template and a stream's
subjects, while the bus module sends, and the grants part needs, mesh-sdk's membership binding: `local` (the local
name), `storage`, `template`, `stream` and `subject` (a stream's, and a ledger's history's subject), `bucket`,
`object-store`, and for a ledger `history`, `state`, `indexes` (each index's bucket by its name), `snapshot` and
`types`, which `created` and `recreated` carry and only `dropped` clears (corrected 2026-11-11, develop question D-11 of design 47's milestone G: an earlier wording had the bus module's events leave them empty). The
definition's `Binding` takes exactly those fields, with those names and forms, so `created`, `recreated` and
`list`'s `binding` carry one form, the generated contract decodes what the bus module sends, and no byte changes
on the wire; the fields design 47 G §399 adds come on top of it. Why: the design's choice following this line's
own words (one value in the events and the answer), met by the developer on 2026-11-11 and directed by lead-47. It is present on every entry whose objects the bus holds (the
entries whose answer carries `settings`) and absent on one it does not hold (waiting to be made, or refused), as a
refused create hands no binding (design 25 bus-state §4). `show` and `orphans`, which answer `Held` too, carry it
alike. A field G's part adds to `Binding` (design 47 G §399: `objects` and `sent-objects`) reaches `list`'s answer
with it, since the form is one. What changes: the seat's definition in mesh-contracts (`seats/mesh-broker`), its
generated Go contract, and mesh-sdk's bootstrap copy of the definition in `go/seatlist`, which its test against
mesh-contracts holds equal; the bus module's tools fill the field. **The size**: 18 entries in the new form are
asked by the manifests on main (counted 2026-11-11), so the answer is a few KiB, an estimate; it stays one reply
well under the bus's largest message (1 MiB) while the entries number in the hundreds, and from G a reply over that
limit goes as an object by design 47 G §407. Why: the design's choice. How it is checked: unit (mesh-contracts' generated contract), a
`created` body as the bus module's keeper writes it, a ledger's among them, decodes into the generated type and
encodes back to the same bytes; unit (the bus module's
tools), a row where `list`'s `binding` for an entry equals the `binding` its `created` carried, and a row where an
entry not held has none; the seat's fixtures.
- §3 **It is asked at every start and when asked again by hand, never on a timer.** The grants part's process asks
`list` once at each start, as it asks its other first views (design 25 bus-state §5), and again when a person
calls `grants_first_view_again` with `state` `bindings`, or with no `state`, which asks every first view (§6).
Nothing else asks it: no timer, and no event, since the bus module's events carry the indexer's revision of the
verdict they acted on and no revision of the bus module's own, so a gap in them cannot be seen (ADR 0615 §5 asks
again on a gap only where one can). **At a start the answer comes first**: the grants part takes no event from its
durable consumer of `mesh-broker.created`, `recreated` and `dropped` until the answer is applied, or until the
bound of §5 passes. Since the shared stream keeps the events in order and each event sets or removes one entry's
whole binding, the events held then, taken after the answer, leave each binding as the last word on it said,
whichever came later. Why: the design's choice under ADR 0615 §4 to §6. How it is checked: §8.
- §4 **How the answer is applied.** For each entry of the answer that carries a binding, the grants part writes the
binding into the module's membership on every machine the module is assigned, kept in its state `memberships` as
on `created`, where it holds none, and rewrites it, as on `recreated`, where it holds a different one; where it
holds the same, it writes nothing. Each binding written or rewritten is said in its log, naming the module, the
local name and the first view as the cause, and recomposes that module's memberships as an event's would. An entry
listed without a binding (§2) changes nothing the grants part holds for it, but for an entry the bus holds in an
answer of a bus module's build from before this milestone, which carries no `binding` on any entry: §9 composes
that binding, and it is then applied by this line as one the answer carried. **A binding the bus no longer
lists is kept, never removed by a first view**: the grants part removes a binding only on `dropped` (ADR 0595 §1,
§7), so a bus module whose own state `asked` was lost or rebuilt, and answers few entries or none, strips no
module of its data. Instead it raises the condition `mesh-grants.binding-unlisted`, one per such binding, naming
the module and the local name and the operator's step (read the entry with the seat's `show`; an entry its
manifest no longer asks for leaves by the bus module's `drop`, whose `dropped` removes the binding); a later answer
that lists the entry, or a `dropped` for it, clears it. **On a re-ask the events keep flowing**: an answer is
applied only to entries no `created`, `recreated` or `dropped` named since that ask was sent, so an answer never
writes over a newer event. Why: the design's choice under ADR 0595 §7, and ADR 0615 §6's rule that a reader keeps
what it holds rather than taking an empty answer as the truth. How it is checked: §8.
- §5 **While the bus module does not answer, the grants part composes the rest and says so.** It composes every
account and keeps every binding it holds in `memberships`, as it does today; only a binding it never heard waits.
Past two minutes without an answer, the bound of its other first views (design 25 bus-state §5), it raises its
condition `mesh-grants.first-view-unanswered` naming the seat `mesh-broker` and the verb `list`, as that condition
names the owner and the tool of its other first views, and it then takes the events from its durable consumer as
they come, so the live path never waits on the first view. It keeps asking, from 10 seconds, doubling, at most
every 5 minutes, the backoff the bus module keeps for its own first view (design 25 bus-state §1); the answer
clears the condition. This form holds until milestone G of design 47 is delivered; from then the grants part, one
of G's observers (design 47 G §387), writes `first-view-unanswered` and `binding-unlisted` (§4) as findings in its
state `findings`, said as `finding-written` and `finding-cleared` on its finding stream (design 47 G §383) and
stated at its start (§384), naming the same things. Why: the design's choice under ADR 0615 §6. How it is checked:
§8; from G, G's own checks of §383 and §387.
- §6 **The tools that read and steer it.** `grants_first_views` shows the first view `bindings` beside the others:
the seat and verb (`mesh-broker.list`), when it was asked and answered, how many entries the answer listed, how
many bindings it wrote and rewrote, how many of those it composed by §9 and the entries §9 left unbound (by module,
local name and storage type), the bindings it found unlisted, how many asks and hand acts, and whether its
condition is raised, kept in the grants part's own state `user-list` under the key `first-views` as the others are.
`grants_first_view_again` takes `bindings` among its `state` values, by a person's act with why, recorded as a hand
act; its description names it. From G these two tools are served by mesh-sdk's `go/views` as every part's are
(design 47 G §245), with `bindings` among the first views they show. Why: tools-are-how-a-module-is-spoken-to. How
it is checked: unit (grants part's tools), `bindings` shown and asked again with its hand act recorded.
- §7 **What exists today, and its fate.** The live-event path stays whole (design 25 bus-state §4). The bus module's
`list`, `show` and `orphans` keep every field they answer and gain `binding`. The grants part reads no other
module's bus state and gains no grant on the shared stream `EVENTS` beyond its durable consumer of the three events:
a missed event is recovered by asking its owner, never by reading the stream back by hand. **The order, and what
the first wording missed.** This line first said the order between the repositories needs no edge of the module
graph, because a grants part meeting an answer without `binding` wrote nothing from it and kept working on the
events. It missed that the bus module's own build goes through the registry (corrected 2026-11-11, issue 1966): its
image build asks the seat `container-image-registry`'s `mirror` for its base image, which the registry holder
serves only with its state `mirrors`, a binding whose `created` the grants part never heard; so the bus module's
new build could not be built while the grants part waited for that build's `binding`, a circle. §9 breaks it: the
grants part composes the key-value and object-store bindings from the running bus module's answer, so it reaches
the machines in a delivery of the grants part (mesh-delivery) alone, with no new build of the bus module (the
operator, 2026-11-11 13:30 CET, in the terminal: "Yes, build it"). The order is then the grants part with §9
before the bus module's build of this milestone, which mesh-contracts, mesh-sdk and the nats module (mesh-catalog)
carry; the edges between the grants part and the bus module otherwise lie on the foundation's cycle, where ADR 0615
§2 has the owner's answer and the reader's wait carry the order.
- §8 **How it is checked.** **Whole**: a case in the grants part's existing whole-module integration test (mesh-delivery
is over its integration budget, so no new test function): a fixture bus module answers `mesh-broker.list` with an
entry whose `created` was never said, and after the grants part starts, the module's membership holds that
entry's binding; a binding the grants part holds that the answer does not list stays and raises
`mesh-grants.binding-unlisted`; with `list` unanswered, the part composes every account, raises
`mesh-grants.first-view-unanswered` naming `mesh-broker` and `list` past its bound, and applies a `created` heard
meanwhile; an answer that comes after a `dropped` heard since the ask does not write the dropped binding back.
**Unit**: the bus module's tools (§2), the grants part's tools (§6), and a table of the grants part's reconcile
with one row per case of §4. **Live**: after the delivery, at the grants part's first start, the registry holder's
membership names every entry its manifest asks for (`grants_membership`), and `grants_first_views` shows
`bindings` answered with no binding unlisted. **§9's cases**: unit rows in the same reconcile table, one per
case of §9 (a key-value and an object-store entry composed; a stream and a ledger entry left unbound and logged;
an entry without `settings` left unbound; an entry whose `settings` lack the bus object named from its `bus-name`
left unbound and logged; an answer carrying `binding` taken as it is, nothing composed). **§9 live**: after the
delivery of the grants part alone, before any new build of the bus module, `grants_membership` for the registry
holder on the machine it runs on lists `holds`, `windows` and `mirrors` beside the entries it held, and
`grants_first_views` shows `bindings` answered with those three composed by §9.
- §9 **An answer without `binding` is composed from the bus module's own fields, for the storage types it names
whole.** A bus module's build from before this milestone answers `list` with no `binding` on any entry; each entry
still carries its `module`, `local-name`, `bus-name`, `storage-type`, `template`, `options`, `fill`, and `settings`
keyed by the bus object's name (a key-value bucket's stream as `KV_` and its bus name, an object store's as `OBJ_`
and its bus name, a stream by its own name). For an entry of such an answer the grants part composes the binding
only when its `settings` hold the bus object its `bus-name` names (so the bus holds it, as §2 has `binding`
present only on a held entry), and only for the storage types whose binding the answer names whole: for
`key-value`, `local` from `local-name`, `storage` from `storage-type`, `template` from `template` and `bucket` from
`bus-name`; for `object-store`, the same with `object-store` from `bus-name`. It composes nothing for a `stream`
(the answer does not carry its subjects) or a `ledger` (it does not carry its parts), nor for an entry whose
`settings` lack its bus object: those are left as today, unbound until the bus module's `created` or `recreated`,
or a later answer that carries `binding`. An answer is taken as one without `binding` only when no entry in it
carries the field; an answer where any entry carries it is applied by §4 alone, nothing composed. **It is the bus
module's own word**: every field the binding takes is the bus module's answer about what it holds, from its seat,
and the grants part adds no name and reads no bus state of its own, so ADR 0595 §1 and §7 (the binding is the bus
module's to say and the grants part's to write) and ADR 0596 §2 hold. **How it is said**: once per answer, a log
line of the grants part naming the seat `mesh-broker` and the verb `list`, that the answer carries no `binding`,
how many bindings it composed and every entry it left unbound by module, local name and storage type; each
binding then written is logged by §4 with the cause "first view, composed from `list`'s fields". `grants_first_views`
shows the same (§6). **Its fate**: it stays after the bus module's build of this milestone is everywhere, since it
acts only on an answer no entry of which carries `binding`, which that build never gives, and covers a bus module
held back or set back to an earlier build. Why: the operator's answer of 2026-11-11 13:30 CET (in the terminal,
to lead-47, for issue 1966), under ADR 0595 §1 and §7; the fields, the storage types and the log are the design's
choice following that answer and the bus module's `list` answer as it stands today (issue 1966, read live
2026-11-11). How it is checked: §8, §9's cases and §9 live.
@@ -0,0 +1,399 @@
---
layer: to-be
design: 25
milestone: bus-state
updated: 2026-11-11
code: [mesh-sdk, mesh-contracts, mesh-delivery, mesh-tools, mesh-catalog modules/nats]
decisions:
- "ADR 0595"
- "ADR 0596"
- "ADR 0597"
- "ADR 0259"
- "ADR 0363"
- "ADR 0386"
- "ADR 0387"
- "ADR 0436"
- "ADR 0564"
- "ADR 0578"
- "ADR 0603"
- "ADR 0612"
- "ADR 0613"
- "ADR 0614"
- "ADR 0615"
- "ADR 0619"
- "ADR 0620"
- "ADR 0621"
- "ADR 0622"
- "ADR 0625"
- "ADR 0626"
- "ADR 0627"
- "ADR 0631"
---
# Design 25, milestone bus-state — what a module keeps on the bus, created by the bus module
**What it builds.** The lines of [design 25](../25-the-bus-on-nats.md) § 12 that are built under issue 1966: who
creates a module's bus state and how the module receives its binding (§ 12.2), the seat `mesh-broker`'s verbs,
events and fill (§ 12.3), the ledger (§ 12.4), the calls (§ 12.5) and the order of the build and the move (§ 12.6).
The three layers and the templates are the design's main file (§ 12.1), and so is how each rule is checked
(§ 12.7). The sections keep their numbers, so "design 25 § 12.2" still names them. *Moved here from the
main file on 2026-11-11, when the main file was put in the shape of playbook 02, and numbered; their words are
unchanged but for the note on bus-state §4 that design 25 bindings-first-view amends it.*
**How a line is cited.** Each statement is cited as "design 25 bus-state §n". A bare "§n" inside a statement is a
clause of the record that statement cites, as it was before the move; "§ 12.n" is a section of this design.
### 12.2 Who creates it, and how a module receives it
- §1 **The trigger is the manifest validator's verdict** (ADR 0595 §4), heard as the indexer's event `manifest-accepted`, which
carries the module's entries whole, so the bus module never reads the indexer's state and never polls. The indexer
says it for every newly accepted manifest, with its entries empty when it asks for none, and the bus module reads an
empty one as "this module asks nothing now", which is what its orphans need. A module whose manifest leaves the
trunk (its directory deleted) gets an empty `manifest-accepted` too, so its entries reach `orphans` and raise
`orphaned`; a manifest the manifest validator set aside to wait gets none, so its entries stay asked. The indexer's tool
`indexer_say_accepted` says `manifest-accepted` again for one module by hand, from the verdict it holds, recorded as a
hand act, for a verdict the bus module is thought to have missed. It goes, as every event of the
indexer, on the module events stream (ADR 0363) under the indexer's own subjects. A publish of it that fails is
published again, and one still failing writes the indexer's finding naming the module, cleared once said (today
the indexer only logs it). It hears the verdicts on a durable consumer, so one given while it was down is heard
when it is back. **Its first view** at start is the indexer's
tool `indexer_bus_state` (every accepted manifest's bus-state entries, by module and digest), asked once; the
events keep it current after that, and a stream that keeps a verdict only for its age is never its source. A
verdict replayed from its consumer is applied only if newer than what the first view answered for that module, by
the indexer's revision alone (a digest has no order), so a replay never takes an entry back. An `indexer_bus_state`
that does not answer makes the bus module's health unhealthy, naming the verb, and is asked again on a bounded
backoff (from 10 seconds, doubling, at most every 5 minutes); nothing is created or orphaned meanwhile. Beside the
backoff, the bus module's tool `nats_first_view_again` asks `indexer_bus_state` at once by hand, recorded as a hand
act. It keeps
what it last heard per module in its own state `asked` (an old `state` section, its own kept data staying there,
ADR 0595 §10), from which `list`, `check` and `orphans` answer; a key goes only on an empty `manifest-accepted` for
the module, never because a manifest the manifest validator set aside to wait is missing from the first view. Then it runs `check` over what it holds, creating what
is missing. The manifest validator checks every entry once, at the merge check and at take-in (ADR 0578; ADR 0595 §8), and also
each ledger type's operations: a set-entry or a remove names `into` and `key`, `fields` and an index's `on` name
fields of the schema, and `digest-of` names a text field.
- §2 **A module that asks bus state waits for the bus module** (ADR 0614 §5). Each bus-state entry of a manifest is a
requirement on the provision `bus-state`, which the bus module's manifest provides; the module graph reads it as any
requirement, so the assigner holds a consumer's new build until a build of the bus module that offers `bus-state`
runs healthy (ADR 0603 §5). That each entry exists before its user starts is the bus module's own work, above, not
an edge.
- §3 **The bus module has a process of its own** beside its tool bundle, with a declared health check. Its credential
creates streams and buckets under modules' names only, and deletes only through `drop` and `recreate`. Its health
turns unhealthy, naming the cause, when an accepted verdict has waited on its durable consumer, unanswered, for
more than three minutes, or when its last pass over fill, which runs every minute, is more than three minutes old, so the only guard against a store filling never stops without a sound; another module's entry past the raise mark
raises that module's condition and leaves the bus module healthy. **Its process has a process account of its own,
granted more than any other process's** (ADR 0626 §1; ADR 0436 §1, §2). The process declares `bus` as any process
may, with one key more, `keeps`, which is allowed on this process alone; the grants part composes the account from
that declaration as it composes every process account, and the vault mints it as the module's own secret. Beyond
what a process's `bus` grants, `keeps` grants the bus's create and update subjects for streams, buckets and object
stores under modules' names only (`<module>_*`), the snapshot request for an entry before `recreate` or `drop` with
its chunks' acknowledgement subjects and its own inbox, and the delete subjects for those two acts. It is granted
no subject on which a tool or a seat's verb is answered (ADR 0436 §3), and no subscription to any restricted
stream's own subjects: the snapshot is the one way a restricted stream's data reaches it, kept only in the bus's
snapshot store, and the process reads no message of it. No other account holds those subjects, the tool runner's
included. **The snapshot account stays as it is** (ADR 0626 §2, § 3 above). **The manifest validator refuses any other bus
declaration of the bus module** (ADR 0626 §3): anything it would say or hear on the bus beyond its process's `bus`
and the bus state the foundation keeps on the old sections (ADR 0595 §10), `keeps` on any other process or module,
and a `bus` of its process that names a subject on which a tool or a verb is answered.
- §4 **The binding travels on the bus module's events** (ADR 0595 §1, §7, as their progressive insight of 2026-11-03
has it, waiting for the operator's Approve). The grants part, which issues every membership, hears them on a durable
consumer: it writes the **binding** (the names on the bus) into the module's membership on `created`, on every
machine the module is assigned, rewrites it on `recreated` and removes it on `dropped`, kept in its own state
`memberships`, so no part writes another's state and a binding outlives the 7 days its event is kept on the shared stream (§ 12.3).
*Amended 2026-11-11 by design 25 bindings-first-view §1 to §5*: the events are no longer the binding's only
source; at every start, and when asked again by hand, the grants part also takes a first view of every binding
from the seat `mesh-broker`'s `list`, so a missed event loses no binding for good. A module whose binding is missing at start fails loudly, naming the entry; it never starts blind. A `refused`
create hands no binding. **A refused or overdue creation is loud**: overdue means no `created` or `refused` within
five minutes of an accepted verdict that asks for the entry. The condition is raised by the watchman, which today raises the manifest validator's
refusals as findings and gains, through its `consumes`, the accepted verdicts and the seat `mesh-broker`'s `created`
and `refused`; never by the bus module, which cannot raise it while it is down; it names the module
and the entry; a later `created` clears it, and so does a later accepted verdict of that module that no longer asks
for the entry.
- §5 **A foundation part's first view of another's state is a tool its owner answers in a runtime of its own** (ADR 0615,
ADR 0621). The four
foundation parts that read each other (the grants part, the tool runner, the indexer and the assigner) stop
reading the indexer's and the assigner's bus state in the milestone of ADR 0612, with the other 19 readers of that
state (ADR 0615 §1). **Each owner serves them as tools of its own module**, the indexer `manifests`,
`modules`, `verdicts` (ADR 0622 §1) and `graph` (ADR 0631 §1), the assigner `placement`, `seat-holders` and `wants`, and declares `runs-as`, so its tools bundle runs in
a runtime of its own on the module's own bus account, never in the node's tool runner, and the tool runner never
asks through itself (ADR 0621 §1). No new manifest form is added: a process still answers no tool call or seat verb
(ADR 0436 §3). **Each verb is granted to exactly its readers** (ADR 0621 §2, as ADR 0615 §3 lists them): `placement`
to the grants part, the tool runner and the indexer; `seat-holders` to the tool runner; `wants` to the grants part;
`manifests` and `modules` to the grants part, the tool runner and the assigner; `verdicts` to the grants part
alone, its `callers` list holding the grants part only (ADR 0622 §1); `graph` to the assigner alone, its `callers`
list holding the assigner only, the indexer itself refused, and the assigner's `reads` entry of the indexer's
`graph` goes (ADR 0631 §1). These seven are the first-view tools, four of the indexer and three of the assigner.
`verdicts` follows the first-view rules whole (ADR 0622 §2, under ADR 0615 §4 to §6): its answer carries the
revision, the grants part then follows `manifest-accepted` and `manifest-refused` and asks once more on a gap in
their revisions, it raises the condition naming the indexer and `verdicts` past the bound and never reads the
indexer's state, and `grants_first_views` and `grants_first_view_again` cover it. `graph` follows them whole too
(ADR 0631 §2, under ADR 0615 §4 to §6): its answer carries the revision, the assigner then follows the indexer's
`indexer.views` events for the bucket `graph`, which carry the revision, and asks once more on a gap, said in its
log; started while the indexer is down, it raises the condition naming the indexer and `graph` past the bound and
never reads the indexer's state; and `assigner_first_views` and `assigner_first_view_again` cover it. A reader declares each in `invokes`
as `<module>.<tool>` only where it is granted, which is the grant the grants part composes as for any tool, and
each tool checks the proved caller against a `callers` list holding exactly its readers and refuses any other, the
owner itself included. **The manifest validator of the milestone is its own group's** (ADR 0621 §4): since no field is added,
the running manifest validator takes the indexer's and the assigner's manifests in as it does today, and the merge check judges
them with the manifest validator built from the group's own head; no release of the manifest validator goes ahead of the milestone. **The
owner's runtime answers before any first view of its own** (ADR 0621 §3): at once from its module's own state, as
soon as it is connected and before it has asked any other part anything, with the revision of its state it answered at,
so the indexer and the assigner, each asking the other at start, never wait on each other (§4). The reader then
follows the owner's declared events, each carrying the revision of the state it follows, and never asks the verb
again on a timer; a gap in revisions makes it ask once more, said in its log (§5). The owners' events are new
families of their own, apart from the indexer's `manifest-refused` and `manifest-accepted` and the assigner's
`put`: the indexer's `indexer.views` and the assigner's `assigner.views`, one event per changed key with its
bucket, key and revision and the key's value whole, or a delete mark for a key removed, so a reader keeps its
picture from the events alone and asks the verb only on a gap (ADR 0615 §5). **Both families are declared in
their owners' old `streams` sections**, each an entry with `max-age-days` 7 and `max-mib` 256: the old section keeps
a stream by limits, which drops its oldest when full (only a work queue refuses the new instead), so a reader that
misses a dropped event sees the gap in revisions and asks once more. The indexer's own kept data stays on the old
sections (ADR 0595 §10 names it among the foundation); the assigner's goes beside its existing `streams` entry
`placement`, under ADR 0619, which extends ADR 0595 §10's foundation to the parts
the bus module's own composition, placement, build or delivery needs before it can answer: the assigner, the tool
runner, the builder, the checker, mesh-config and the vault. **The size, an estimate**: read on the bus on
2026-11-04, the buckets the readers read hold about 3 MiB of current values at the indexer (its manifests about
5 KiB a value) and about 1.5 MiB at the assigner (its wants about 0.25 KiB a value). The milestone's one mesh-sdk
rebuild rewrites every module's keys, up to three times in its window (design 51 *How many builds*), so a week of
it is some 20 MiB on `indexer.views` and some 10 MiB on `assigner.views` with ordinary merges added, each well
under its 256 MiB. Each event carries one value whole, the largest today a manifest of tens of KiB, under the bus's
largest message, 1 MiB (the bus server's `max_payload`); an owner whose value with its event would pass it emits
the key's revision alone, as a gap, so the reader asks the first view once, and the owner writes its finding
naming the key.
A waiter on an existing event, `manifest-accepted` or the
assigner's `put`, never wakes on them. **When an owner's revisions go back**: should an owner's state be rebuilt
by the bus module's `recreate`, its revisions start again; a reader that hears a revision lower than the one it
holds takes it as a gap and asks the first view again, and keeps what that answer says, whatever revision it held
before. A `recreate` that could not keep the data answers empty at revision 0, so the grants part keeps its list
in force (below): a login revoked meanwhile stays until the owner's first change, and the grants part's
first-view condition stays raised meanwhile, naming the owner, the empty answer and the operator's step: let the
owner write its state again from its sources (the indexer by taking each module in again, `indexer_take_in`; the
assigner by its next composition of the wants), then ask the grants part's first view again,
`grants_first_view_again`. It is cleared by an answer above revision 0. A reader that starts while its owner is down raises a
condition naming the owner and the verb past its bound and keeps asking until it answers, never reading the
owner's state instead; once it had its first view, it keeps working on what it heard (§6). The bound is longer
than an owner's start (two minutes), so a normal start, readers and owners coming up together, notifies nobody;
the owner's answer clears the condition. An answer that is empty at revision 0 (an owner that holds nothing yet)
is a first view of nothing, never a reason for the grants part to drop a login it holds: it keeps the list in
force until an answer at a revision above 0. Each reader's tools say the owner, verb and revision its first view
holds, when it asked and when it was answered (a read tool), and ask the first view again by hand (a re-read tool,
recorded as a hand act): the grants part's `grants_first_views` and `grants_first_view_again`, the tool runner's
`mesh_first_views` and `mesh_first_view_again`, the indexer's `indexer_first_views` and
`indexer_first_view_again`, and the assigner's `assigner_first_views` and `assigner_first_view_again`. The tool
runner's two are tools of the mesh MCP server, beside `mesh_runtimes`, since the tool runner serves its own state
there: the mesh MCP server gains them, and the operator sees them among its tools. **On a fresh mesh** the
assigner starts before any new-form bus state exists and needs no binding of the bus module first: its own state
and `assigner.views` are on its old sections, which the grants part creates, so it answers the indexer, the grants
part and the tool runner their first views while the bus module still waits on it. The order of owner before reader is kept by the module graph's served-verb edge for the 19 readers (ADR 0614 §2), and,
for the four, whose verb edges lie on cycles the graph leaves out (ADR 0614 §3), by the owner's answer at once and
the reader's wait (ADR 0615 §2).
- §6 **The other 19 readers ask the owners' read tools, one per bucket they read** (ADR 0615 §1, §2). Beside the seven
first-view tools above, each owner serves a read tool for every bucket of its state one of the 19 reads today,
named `indexer_read_<bucket>` or `assigner_read_<bucket>` and marked as a read: fifteen. The indexer's nine:
`indexer_read_manifests`, `indexer_read_modules`, `indexer_read_predefined`, `indexer_read_config_candidates`,
`indexer_read_candidates`, `indexer_read_packages`, `indexer_read_graph`, `indexer_read_heard` and
`indexer_read_taken`. The assigner's six: `assigner_read_placement`, `assigner_read_seat_holders`,
`assigner_read_wants`, `assigner_read_findings`, `assigner_read_asks` and `assigner_read_differences`. These are
the nine buckets of the indexer and the six of the assigner the 19 read (§ 12.6), `modules`, `wants` and
`placement` among them for the status view; the indexer's tenth, `verdicts`, is read by the grants part alone,
through its first-view tool. **They are ordinary module tools**: granted, as any tool is, to every
reader that declares it in `invokes`, and callable by a session; the `callers` list of ADR 0621 §2, ADR 0622 §1 and ADR 0631 §1 covers
only the seven first-view tools. **The status view keeps all its tools**, their names, arguments and answers
unchanged, its `indexer_*` and `assigner_*` tools included: as a reader it answers them by asking the owners' read
tools instead of reading their buckets. Checked live after the delivery: a session's call of each status view tool
answers, and `mesh_search` finds each owner's read tool by its bucket's words.
- §7 **Only the owner reaches it** (ADR 0596 §2). The membership grants the owner's account its own entries and no
other module's: the new form has no readers. What another module needs of that data it asks through the owner's
tools or seat verbs, and what the owner lets others hear is its declared events, subscribed to through
`consumes` (ADR 0363, unchanged). A part watches only its own state, and hears another's events (ADR 0596 §4).
### 12.3 The seat's verbs, events and fill
- §8 **The seat gains eight verbs.** The seat `mesh-broker`, until now a seat with no verbs, gains these (ADR 0595 §5, §6). It is one of the ten seats
defined whole in the milestone of ADR 0612, its contract a package of its own in `mesh-contracts` (ADR 0613 §1,
ADR 0627 §1, ADR 0625 §1; design 26 *A seat's definition*), which settles where its definition lives (ADR 0595 §12). An entry's arguments are the module, its local
name, its storage type, its template, its options and its configuration; what is held answers the settings held and
the fill (count, bytes, the cap); `drop` and `recreate` carry the warrant and why.
| Verb | Does | Who may call it |
|---|---|---|
| `list` | every entry, its template, its settings, its fill against its cap | any caller whose role grants it (ADR 0386) |
| `show` | one entry, the same | the same |
| `check` | compares what the bus holds with what was asked, entry by entry | the same |
| `orphans` | entries the bus holds that no manifest asks for; never one whose manifest the manifest validator refused or outgrew, nor one of the foundation's old sections | the same |
| `create` | creates one entry now, for an entry an accepted manifest asks for | an act of the bus module's process alone, answered on no subject |
| `apply` | applies a difference that only grows: a larger cap, a longer age | the same |
| `recreate` | rebuilds an entry whose settings differ in a way that cannot grow, from its data where the storage type allows | the same, on the operator's warrant, which the process verifies itself |
| `drop` | removes one orphan | the same, on the operator's warrant, which the process verifies itself |
- §9 **Four of them are acts of the process alone.** `create`, `apply`, `recreate` and `drop` stay in the seat's definition as acts its holder's process alone performs,
answered on no subject (ADR 0626 §4, ADR 0436 §3): they are not verbs whose subjects a credential is granted, but
acts on the bus's own create, update and delete subjects, which only the process's account holds, so no module can
ask the bus for them. A person asks for them through the bus module's tools, which hand the request to the process
through its own state (below); `recreate` and `drop` act only on the operator's warrant (ADR 0259 §6), asked
through the operator channel. Before `recreate` or `drop` acts, the process takes a stream snapshot of the entry
(ADR 0595 §5), and it acts only once the snapshot is whole. A drifted entry is said, never quietly fixed: only a
difference that grows is applied without a person.
- §10 **Events**: `created`, `refused` (with why), `drifted` and `drift-cleared`, `full` and `full-cleared`, `orphaned`
(cleared by a `drop`, or by a manifest asking for the entry again, which emits `created` for it), `recreated` and
`dropped` (ADR 0595 §6, as its progressive insight of 2026-11-03 has it, waiting for the operator's Approve);
each carries the module, the local name, the bus name, the template, and the settings asked against those held.
**They follow every seat's events**: each is said on `mesh.seat.mesh-broker.event.<event>`, on the shared stream
`EVENTS` (§ 3), kept 7 days, and heard through `consumes` `mesh-broker.<event>` like any seat's event. **The bus
module keeps their history apart**, because a drift or an orphan is read back days later by a person: a stream of
its own in its old `streams` section, on its module subjects, kept 30 days (ADR 0363's bound), created by the grants
part as today, with a cap of 256 MiB, to which the process appends every event it says. Its own kept data stays on
the old sections (ADR 0595 §10), because it cannot wait for its own process to create it. That stream drops its
oldest when full, which loses no live state, because every drift, orphan and fill stands as a condition until it
clears; the conditions hold the live state. The tool `history` of one entry reads it back: every event the bus
module said of the entry, in order. This settles where they are kept, which ADR 0595 §12 left to the design, and
needs no new manifest form: a module's stream never takes a seat's subjects, and every seat keeps one form for its
events.
- §11 **Fill is the bus module's one periodic read** (ADR 0595 §6): every minute, for every entry that refuses when full,
because the bus says nothing as a store fills. The condition `full` is raised at a raise mark and cleared below a
lower clear mark, so an entry sitting at the mark does not flap; the marks are settings of the bus module, by default
a raise at 75 percent of the cap and a clear below 70 (ADR 0595 §6, as its progressive insight of 2026-11-03 has it).
The grants part's `mesh-grants.history-full` moves to it: the new condition is raised first, then the old key cleared;
no manifest on main asks for a history stream or objects today, so the move loses nothing. A bare stream that refuses
when full is read the same way. **The conditions**, each naming the module and the entry: `full` (cleared below the
clear mark), `drifted` (cleared by `drift-cleared`), `orphaned` (cleared by a
`drop` or by a manifest asking for it again) and a refused create (cleared by a later `created`).
- §12 **Its tools** (tools-are-how-a-module-is-spoken-to): the four reading verbs, marked reads, answer every entry the
bus module created and its fill, and `history` its events for one entry; `create` and `apply`, marked acts, and
`recreate` and `drop`, marked dangerous and so granted only by name (ADR 0386 §6, ADR 0387), are its tools for a
person. **A person's request reaches the process through the process's own state** (ADR 0626 §4), because a process
account answers no tool call (ADR 0436 §3). Such a tool, in the bus module's bundle, holds no grant of the bus's
create, update or delete subjects, and neither does the tool runner's account: it writes the request into the bus
module's state `requests` (an old `state` section, as `asked` is, ADR 0595 §10), stamped with the caller the tool
runner proved and recorded (ADR 0386 §8), and watches that key for the process's answer, never asking again on a
timer. The process watches its own state `requests` (ADR 0596 §4) and takes a request up when it is written. The stamp names
who asked, for the record; it is no boundary, because every session on a machine reaches the bus as the tool
runner's account (ADR 0436 §9). **So the process verifies the operator's warrant itself**: for `recreate` and `drop`
it trusts nothing in the request, reads the operator channel's record of the ask under its own account and checks
that the warrant binds exactly this act on this entry (ADR 0259 §6), as the mail sender does (ADR 0436 §9); a request
without a valid warrant it refuses, deleting nothing and writing why into its answer; with one, it takes the entry's
stream snapshot first. **The process wakes on the warrant, never on a timer**: a warrant is said on the operator
channel's `decided` for the asker that raised the ask (ADR 0259 §6; ADR 0620 §2), here the bus module itself, which
hears its own `decided` on a durable consumer of the process, whose filter the grants part derives from the
process's `uses` of the operator channel seat (not from a `consumes`, which a process has none of), and runs its pass over the waiting `recreate` and
`drop` requests when one arrives, so a warranted `drop` acts within seconds of the operator's answer, and one given
while the process was down is heard when it is back. **A warrant acts once** (ADR 0259 §6): before it acts, the
process claims the warrant's ask id by writing the key `acted.<ask id>` in the state `spent` of the process's own bus state, which the tools bundle is not given (a person's request and the
record of what it spent are never in one bucket, and no tool can write the record), created by compare-and-set after the snapshot and before any deletion, and a request naming an ask it already
acted on is refused, deleting nothing and saying which act that warrant already performed. **An ask lasts 12 hours**,
shorter than the 24 hours `requests` keeps a request: a request whose ask ended unanswered is ended and said, and a
warrant is not acted on once its ask ended; the tool raises the ask before it hands the request over. A request
that cannot act yet says why in its field Waiting, and a hand act the process could not record is written on the
request in its field Unrecorded. The tool `nats_requests` reads the waiting and ended requests, and the warrants
spent from `spent`, which is declared `read-only: true` (design 32 § 4) so that the tool reads it and holds no publish on it, and drops a waiting request by hand, recorded as a hand act. `create` and `apply` need no warrant, since they only make or grow what an accepted manifest
asks for, and the process refuses either for an entry no accepted manifest asks for. A tool acts only for a caller
whose role grants it; the process records each act as a hand act (`mesh-hand-acts`) and answers what it did. What
stands between the tool runner's account and the process's delete power is that check of the warrant, so its tests
are this section's central check (§ 12.7, in the main file). **The mesh deletes kept data only through `drop` and `recreate`** (ADR 0595 §4, as its
progressive insight of 2026-11-03 has it): the bus module's older remove tools (its stream, bucket and consumer
removals, ADR 0564) refuse an entry of the new form, naming `drop`'s tool, and keep working for the old sections. The bus seat's `apply` (a difference that grows) and the
ledger's `apply` (§ 12.4) are two verbs on two subjects, granted apart; this design writes them qualified. Its health
names the process's last pass, the entries it could not create, and the entries past the raise mark.
### 12.4 The ledger
- §13 **The ledger is a storage type of its own.** A **ledger** is a storage type whose state is derived from its history (ADR 0597). It is built on a stream and
key-value buckets, and asked for with the template ledger.
```
module ──apply(instance, operation)──▶ tool runner on its own machine
│ 1 checks the operation against the type
│ 2 reads the history's last sequence for the instance; replays if the state is behind
│ 3 appends to the history, that sequence expected ──▶ history (stream, a subject per instance)
│ 4 derives the state by the operation's effect ──▶ state (key-value, updated at the revision read)
│ 5 writes the instance's snapshot ──▶ snapshot subject (latest only, never expires)
▼
answers: taken, with its sequence (and "state behind" when the state bucket is full)
module's process: a watcher keeps each declared index (a second key-value bucket) from the history
```
- §14 **A type** is a schema and the operations allowed on it; an **instance** of a type is named by a path
(`listing/a17`, `decision/0290`); every change is an **operation**, kept in the instance's history.
- §15 **A ledger effect** is what an operation does to the state, one of a fixed set: set fields, set an entry of a
map, append to a list, remove (ADR 0597 §2). An effect copies only the fields its type names, and may add the
operation's sequence and author. Where a type pairs a text field with its digest, `apply` refuses an operation
whose digest does not match. A module's own rules are checked by the module before it calls `apply`, never by the
effects. Only the owning module calls `apply` on its ledger, for every effect, remove among them; a person
removes through the owner's tools. This settles who may call the remove effect, which ADR 0597 §13 left to the
design.
- §16 **One writer wins, and the state never goes back** (§5): the expected sequence makes the bus refuse a racing
writer; the state is written as an update of the revision read, never a plain put; when that update loses to
another writer, `apply` answers success with its operation's sequence and re-reads, never appending again.
- §17 **A full state bucket loses no operation** (§6): `apply` answers that the operation is taken, with its sequence,
and that the state is behind because the bucket is full; the next `apply` after room is made catches it up.
- §18 **The author is proven** (§7): `apply` stamps the owning module's authenticated account and the account that
called the owner's tool. The owner passes that author on by naming the tool call it serves: the tool runner, which
served that call and proved its caller, stamps the caller it proved, so the module cannot set the author to words of
its own; an `apply` naming no call in flight carries the owner's account alone, and one naming another module's
call is refused. A copy of old records (design 55 § 8.1) is stamped with the owner's account and keeps the old
author as a separate field marked imported, never as the proven author.
- §19 **Every instance has a snapshot** (§8): the latest state on a subject of its own, never expiring, written by every
`apply`. It makes a kept ledger's rebuild fast and an expiring ledger's possible; a repair that finds it behind
the state rewrites it.
- §20 **Only the tool runner's ledger credential publishes on a ledger's history** (§9): one per module assignment, that
is per node the module runs on, the account `ledger.<node>.<module>` in the tool runner's own secret `ledgers` on
that node (ADR 0597 §9, as its progressive insight of 2026-11-03 has it, waiting for the operator's Approve), held by
the tool runner and never by the module's own code, whose account may read its ledger and not publish on it. A
person's `rotate` of that secret is refused by the vault, as a bus account's is. The grants part issues it with the
module's membership on that machine, scoped to the subjects of that module's ledger
histories and snapshots, and removes it when the module is unassigned from the machine. Only the
owner's account reads the history, the snapshot and the state.
- §21 **An index** is a second key-value bucket kept by a watcher in the owning module's process, rebuildable from the
history; it raises a condition when it lags behind (§4).
### 12.5 The calls, and where they live
- §22 **The new calls live in a package of their own.** The new calls live in a small mesh-sdk package of their own, served by the tool runner and imported only by the
modules that use them; the module library, which 112 modules import, does not change (ADR 0595 §9). They are the
object store's (put, get, list, delete, info, watch); a stream's read-back and fill; a work queue's size and dead
letters; and the ledger's: `apply`, `get`, `history`, `watch`, `query`, and its repairs `rebuild` (the state from
the history or the snapshot), `reindex`, `snapshot` and `lag` (ADR 0597 §10). The tool runner serves the repairs as
tools of every module that owns a ledger, limited to its own ledgers, so no owner declares them and the next ledger's
owner has them on its first day. mesh-sdk's catalog and membership
packages change once for the typed `requires` entry and the binding, rebuilding about 30 modules.
### 12.6 The move, and the order of the build
- §23 **The build, each step undoable on its own** (ADR 0595 §10): (1) mesh-sdk's typed `requires`, binding and calls
package; (2) the manifest validator's rules; (3) the bus module's process, verbs and events, creating only for the new form, with the grants part's writing of
the binding on `created`; (4) the tool runner's calls, the ledger's `apply` and its ledger credential among them; (5)
modules move, each owner first. Each undo: undoing step 2 refuses the new form again, which no module uses before
step 5; undoing step 3 leaves what it created in place, and a module moved after it fails loudly on its missing
binding; undoing step 4 leaves a moved module without its calls, which the placement holds off that machine. The
placement holds a moved module off a machine whose tool runner does not serve the typed binding and the calls the
module asks for (a ledger's `apply` among them), and says so; where the placement reads what a tool runner serves is
left to the build of step 4 and tracked on issue 1966.
- §24 **A module moves when it is next touched** (ADR 0595 §11), keeping its local names and carrying its old settings
as options, so its bus objects and data stay and no drift is raised. It asks for each entry once: an old section
and the new form are never both used for one local name. During the move the grants part keeps creating the old
sections' entries, so each entry has one creator. The indexer counts the modules on each old section, a tool reads
the count, and the manifest validator refuses a section once its count is 0. The foundation's own kept data (the bus module's,
the grants part's and the indexer's) stays on the old sections for good and is not counted: those modules cannot
wait for a process that needs them first. ADR 0619, extending ADR 0595 §10's foundation, adds the parts the bus module's own composition, placement, build or delivery needs before it can answer:
the assigner, the tool runner, the builder, the checker, mesh-config and the vault; their new state and streams of
this milestone go on their old sections too (§ 12.2; design 51 *The move*; design 32).
- §25 **Readers move after their owners** (ADR 0596 §3). Today 195 `reads` entries in 37 modules read another module's
bus state (counted 2026-11-03 02:00). **A correction of fact**: the indexer's and the assigner's state, counted again
from every manifest on main later that day, is read by 23 modules, 10 buckets of the indexer (`verdicts`, read by the grants part, among them) and 6 of the assigner,
not by the 19 the first plan counted; and 151 state entries were counted, not 158 (the template table of § 12.1 keeps
the first count, which chose the templates). The owners first gain their events, first-view tools and read tools: the
indexer a read tool per bucket its readers read, the assigner the same (§ 12.2 lists them). Then all 23 readers move in the milestone of ADR 0612, the
four foundation readers included, by the first view of § 12.2 (ADR 0615 §1), the grants part's of the indexer's
`verdicts` by its first-view tool `verdicts` (ADR 0622) and then `manifest-accepted` and `manifest-refused`, and the
assigner's of the indexer's `graph` by its first-view tool `graph` (ADR 0631) and then `indexer.views`; their order is the module graph's, not
a review reading an owner delivered (ADR 0615 §2). The status view's move keeps every tool it serves, each answered by
asking the owners' read tools of § 12.2. **The node-engine's reads granted in code stay**: the grants part's code
grants each node's node-engine, with no manifest declaring them (`CandidateReads` and the composed reads beside it),
the indexer's `candidates` and `verdicts` and its own node's keys of the assigner's `wants`. They are not among the
23 readers of ADR 0615 §1 and move in the next milestone, queued on issue 1966. The manifest validator still refuses a moved module's subscription to an event
its owner does not declare. Every other reader moves when its module is next touched. These moves are tracked by
issue 1966.
+200 -8
View File
@@ -7,9 +7,28 @@ code:
- mesh-tools src/broker-amqp.ts (to be replaced)
- mesh-catalog modules/nats (to be written)
- mesh-sdk src (the protocol's NATS binding, step 3)
- mesh-tools node-tools/internal/bus (a module's state, ADR 0201)
updated: 2026-10-10
- mesh-tools node-tools/internal/bus (a module's state, ADR 0201; the new calls and apply, ADR 0595, ADR 0597)
- mesh-sdk (the typed bus-state entry, its binding and the new calls' package, ADR 0595)
- mesh-delivery (the manifest validator's rules and the grants part's binding, ADR 0595)
- mesh-contracts seats/mesh-broker (the seat's definition: `list` answers each entry's binding)
updated: 2026-11-11
decisions:
- "ADR 0595"
- "ADR 0626"
- "ADR 0631"
- "ADR 0620"
- "ADR 0259"
- "ADR 0627"
- "ADR 0436"
- "ADR 0619"
- "ADR 0615"
- "ADR 0621"
- "ADR 0622"
- "ADR 0614"
- "ADR 0613"
- "ADR 0612"
- "ADR 0596"
- "ADR 0597"
- "ADR 0289"
- "ADR 0264"
- "ADR 0235"
@@ -38,6 +57,17 @@ under which subject, with which guarantee, under whose account; how a node joins
reaches a tool; how the mesh moves from the bus it has to this one; and how each claim is checked.
Prose and diagrams only; no configuration is pasted.
**Amended, 2026-11-03** (ADR 0595, ADR 0596, ADR 0597): what a module keeps on the bus is its **bus state**, a
provision of the bus module asked for by template, created by the bus module, read by its owner alone; and the
**ledger** is a storage type of its own. § 12 is the design; the `KV_<module>_<name>` row of § 3 and the module's
state in § 4 hold for a module still on the old sections.
**Amended, 2026-11-11** (ADR 0595 §1, §7; ADR 0615 §3 to §6; the operator, 2026-11-11, in the terminal: "Ask the
bus module"): the grants part no longer learns a binding only from the bus module's live events; at every start and
when asked again by hand it takes a first view of every binding from the seat `mesh-broker`'s `list`, so a missed
`created` loses no binding for good (milestone bindings-first-view). The same change puts this design in the shape
of playbook 02: § 12.2 to § 12.6 moved, unchanged and numbered, into the part of milestone bus-state.
## 1. What the bus is for
**The bus is where the mesh happens.** Not a transport the mesh sends things over — the place a
@@ -129,7 +159,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
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
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.
**Revised 2026-09-26** ([design 29](32-what-a-module-declares.md)): a module's events and tools
@@ -179,7 +209,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 |
| 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 |
| `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. *2026-11-03, ADR 0595:* a module's kept data in the new form is created by the bus module from `requires`, by template (§ 12) |
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.
@@ -193,6 +223,9 @@ holding `mesh-broker` — has an account of its own whose only grant is the snap
names, a stream's information, the snapshot request and its acknowledgements, and its own inbox. The
night's backup takes every stream through it ([to-be 43](43-backups-against-mistakes.md)). The
controller stays the only writer of stream definitions; the writers table holds that at composition.
*Amended 2026-11-04, ADR 0626 §2:* that account keeps exactly this grant; beside it, the bus module's process holds
a process account of its own for the bus state it creates (§ 12.2), so the module holding `mesh-broker` holds two
accounts, each doing only what its record grants.
### The store window, and what moving it into the server changes
@@ -266,7 +299,7 @@ expresses this exactly, per subject, and better than a vhost could:
permissions for each consumed event's subject, its tool subjects, and that same inbox prefix.
Nothing else. A module that tries to publish outside its emits is refused by the server, not by
convention.
- **A module's state** (ADR 0201), for whichever principal carries the module — today the machine's
- **A module's state** (ADR 0201; for the new form, § 12.2: the owner's account alone, no reader, ADR 0596), for whichever principal carries the module — today the machine's
runtime, whose grant is the union of its modules': binding to the bucket, reading a key directly,
and an ordered consumer for listing and watching, created and deleted on the bucket's own stream
and nothing else's; and, for the owner's instances only, publishing under the bucket's own
@@ -321,7 +354,7 @@ signing hierarchy for nothing.
## 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
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:
@@ -410,7 +443,7 @@ bridged. It is three things:
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.
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.
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.
@@ -606,7 +639,7 @@ find what changed and why.
decides it.
- The heartbeat interval and the controller's "quiet" threshold on core NATS without persistence —
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.
- 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.
@@ -617,3 +650,162 @@ find what changed and why.
that needs it? ADR 0039's test —
*does editing it recompile unrelated modules, and does it change often* — probably says no for
one instance; worth asking again if a second module needs the same shape.
## 12. Bus state: what a module keeps on the bus, and the bus module that provides it
*Added 2026-11-03* (ADR 0595, ADR 0596, ADR 0597, from research 082; the operator, 2026-11-02 20:45: "The names
simply represent named configurations or templates"). Until now a manifest declared what a module keeps on the bus
in four sections with four vocabularies (`state`, `logs`, `streams`, `objects`), the grants part created each
entry once and never changed it, and nothing compared what the bus holds with what was asked. This section is the
design of the one form that replaces them; [design 32](32-what-a-module-declares.md) § 4 says how a manifest asks
for it, and [design 55](55-the-knowledge-base.md) § 2.1 is its first ledger.
**Bus state is a provision of the bus module** (ADR 0595 §1). The **bus module** is the module holding the seat
`mesh-broker` (today `nats`). A module asks it for each piece of its **bus state** the way it asks postgres for a
database: an entry in `requires`, by its local name, its storage type, its template and the template's options.
The bus module creates it; the module receives the names on the bus in its membership; nothing else creates it.
### 12.1 Three layers
```
layer 3 templates state events work queue log objects ledger
│ │ │ │ │ │
layer 2 storage types key-value │ │ │ object store ledger
built on the bucket │ │ │ │ (a stream and
stream │ │ │ │ │ key-value buckets)
└──────────┴───────────┴──────────┴──────────┴────────────┘
layer 1 base type stream
```
A **storage type** is how the bus keeps the data: the **stream** is the base, and the **key-value bucket**, the
**object store** and the **ledger** are built on it, each with calls of its own (ADR 0595 §2). A **configuration**
is the settings a storage type is given. A **template** is a storage type with a configuration, predefined and
named: six exist, from the uses counted on main (158 state entries, 6 event streams, 4 logs, 3 work queues). An
**option** is one setting of a template a module may change, within its bound. A module gives a configuration of
its own only where no template fits: the bare stream, or a ledger whose history expires.
**What keeps the data safe is fixed, never an option** (ADR 0595 §3). Each template fixes what happens when it is
full, and the manifest validator refuses a manifest that gives it:
| Template | On | Fixed | Options, their bounds (default) |
|---|---|---|---|
| state | key-value | refuses when full; a value at most 256 KiB; the bucket at most 64 MiB; one copy, on disk | past values 1 to 64 (1), 0 refused; time to live (none); a lifetime per key; one bucket per machine |
| events | stream | drops the oldest when full; the module's own subjects | age 1 to 30 days (7); cap 1 to 8192 MiB (1024); a grant (ADR 0363 §4) |
| work queue | stream | removed once taken; refuses when full; one taker; no grant | deliveries 1 to 10; duplicate detection up to its age; cap; age |
| log | stream | refuses when full; never rewritten; an entry at most 256 KiB; one subject per key | cap 1 to 8192 MiB (1024); age 1 to 3650 days (none) |
| objects | object store | refuses when full; kept for good | cap 1 to 8192 MiB, required; a grant |
| ledger | ledger | as log, plus its type and a snapshot per instance; its state and index buckets as state | cap; keeping days only in a ledger's own configuration |
| (none: a bare stream) | stream | the module's own subjects; within the mesh's bounds | its own configuration, which must give "when full" (refuse or drop the oldest), a cap of 1 to 8192 MiB and an age of 1 to 3650 days or none, and may give a grant; the manifest validator refuses one missing any of the three |
The template log is the **module log** of ADR 0297. The mesh, never the module, names the bus objects and their
subjects (ADR 0201 §4): `<module>_<local name>`, and a local name holding the separator is refused so no two
modules' names meet.
### 12.2 to 12.6 Who creates it, the seat's verbs and events, the ledger, the calls and the move
```
manifest validator ─verdict: accepted with the module's bus-state entries whole─▶ bus module
│ creates each entry
▼
grants part ◀──────────────── created, recreated, dropped (module, local name, bus name, binding)
│ ◀───── at its start, and when asked again: mesh-broker.list (every entry, its binding)
│ writes the binding into the module's membership
▼
the tool runner reads the membership ──▶ the module's calls reach its own entries, and only those
```
These sections are the lines of milestone **bus-state**, in its part
[25-milestones/bus-state.md](25-milestones/bus-state.md), where they keep their numbers. The shape, in short: the
indexer's `manifest-accepted` carries a module's bus-state entries whole to the bus module, whose process creates
each entry under the module's names and says `created`, `refused`, `drifted` and `drift-cleared`, `full` and
`full-cleared`, `orphaned`, `recreated` and `dropped` on the seat `mesh-broker`. The seat's reading verbs `list`,
`show`, `check` and `orphans` are answered by the bus module's tools; its acts `create`, `apply`, `recreate` and
`drop` by its process alone, the last two on the operator's warrant and after a stream snapshot. The grants part
writes each entry's binding into the module's membership, kept in its own state `memberships`, from those events
and, at every start and when asked again by hand, from its first view of the seat's `list` (milestone
**bindings-first-view**, in its part [25-milestones/bindings-first-view.md](25-milestones/bindings-first-view.md)).
The ledger is a storage type whose state the tool runner derives from its history; the new calls are a mesh-sdk
package of their own; and the foundation parts take their first views of each other's state from the owner's own
tools, then follow its events.
### 12.7 How it is checked
| Rule | Checked by |
|---|---|
| A manifest's bus state is valid, once | the manifest validator's unit tests, each refusal naming the field: an unknown template, an option outside its bound, past values 0, "when full" given, a fixed setting given, a name used twice across storage types, two modules' bus names alike, a `reads` in the new form, an effect outside the fixed set (ADR 0595 §8, ADR 0596 §2, ADR 0597 §2) |
| The bus module creates, and the binding follows | live, by tool call: a module asking for a state, a log, objects and a ledger in `requires` receives each binding in its membership, written after `created`, never before; `show` gives each entry's template, settings and fill; a module calling `create` is refused |
| The bus module runs whole | one integration test in the nats module's repository that starts the bus module whole against the real bus server: a verdict, then `created`, then the binding in the membership; a restart that catches up a verdict given while it was down; a bucket whose settings differ raises `drifted`, the seat's `apply` of a larger cap clears it with `drift-cleared`; a refused create raises its condition and hands no binding; the process connects on its own process account, composed by the grants part with that account's grants and limits, never on an account without them; a `create` handed through `requests` is answered on the same key, which the tool watches; `recreate` and `drop` without the operator's warrant are refused, and a warranted `drop`, its warrant read from the operator channel's own record of the ask under the process's account and never from a stand-in, takes the entry's stream snapshot before it deletes, and acts within seconds of the bus module's `decided`, with no timer between; the same warrant handed in a second request is refused and deletes nothing; each event said is on `mesh.seat.mesh-broker.event.<event>` and appended to the bus module's history stream, and `history` reads it back; an orphan is listed, raises `orphaned`, and goes only by `drop`; each template at a small cap refuses, events drop the oldest, and `full` is raised at the raise mark and cleared only below the clear mark; `recreated` rewrites the binding and `dropped` removes it; at start the first view from `indexer_bus_state` creates what a verdict given while it was down asked; the older remove tools refuse a new-form entry, naming `drop`'s tool |
| A module asking bus state waits for the bus module | the module graph's table test: a manifest with a ledger entry has a requirement edge to the module providing `bus-state`, one without has none (ADR 0614 §5) |
| A foundation part's first view | unit tests of each owner and reader (ADR 0615's How it is checked), as table cases, and these cases of the whole-module integration tests that exist today (mesh-delivery is over its integration budget, so no new test function): the indexer's (`modules/mesh-delivery-indexer`, its one whole-module test, which the milestone's packages test merges into), the assigner's (`modules/mesh-delivery-assigner`) and the grants part's (`modules/mesh-grants`), and the tool runner's in mesh-tools: the owner answers its verb at once from what it holds, with its revision, before its own first view; the reader asks once at start, then only on a gap in revisions; a reader whose owner is down raises its condition only past the bound and never reads the owner's state, and the owner's answer clears it; an empty answer at revision 0 drops no login; an owner's event carries the changed value whole, and a reader applies a put and a delete from the events alone and never asks the verb; a revision lower than the one held makes the reader ask its first view again; an existing waiter on `manifest-accepted` or on the assigner's `put` does not wake on the new families; for the indexer's `verdicts` (ADR 0622), called by the grants part while the node's tool runner is stopped, it answers, and from any other account, the indexer's own included, it is refused; a missed revision on `manifest-refused` makes the grants part ask `verdicts` once more; the grants part and the indexer started at the same moment, `verdicts` answers within the bound with its revision; with the indexer down past the bound the grants part raises the condition naming the indexer and `verdicts` and never reads the indexer's state, and the answer clears it; for the indexer's `graph` (ADR 0631), called by the assigner it answers with its revision, and from any other account, the indexer's own included, it is refused; a missed revision of the bucket `graph` on `indexer.views` makes the assigner ask `graph` once more; started with the indexer down, the assigner raises past the bound the first-view condition naming the indexer and `graph` and reads nothing of the indexer's state, and the answer clears it; the read tool shows the revision each reader holds, and the re-read tool asks once and records the hand act; in the assigner's whole-module test, the assigner starts before any new-form bus state exists, with no binding of the bus module, and the indexer, the grants part and the tool runner each get their first view of it. Live, after the delivery: the indexer's `indexer_old_sections`, which the milestone extends to count the `reads` entries on each owner's state, reads 0 declared `reads` entries on the indexer's state and 0 on the assigner's, and says the node-engine's reads granted in the grants part's code remain (§ 12.6); and each owner's read tool of § 12.2 answers what its bucket holds, each status view tool answers a session's call as before the move, and `mesh_search` finds each owner's read tool |
| The owners' first views are their own tools, in a runtime of their own (ADR 0621) | cases of the indexer's and the assigner's whole-module integration tests: each first-view tool, called from a reader's account while the node's tool runner is stopped, answers (§1); each answers its granted readers and refuses every other account, the owner's own included (§2); the two runtimes started at the same moment, each asking the other's first view at start, both answer within the first-view bound with their revisions (§3); and the milestone's group passes its merge check with no change to the manifest validator's known fields for these verbs (§4) |
| The grants part's first view of the bindings (design 25 bindings-first-view) | the cases of design 25 bindings-first-view §8: a case in the grants part's existing whole-module integration test, where a binding whose `created` was never said is written after a start; unit rows of the bus module's tools and the grants part, §9's rows among them (a binding composed from an answer without `binding`); live, the next start after the delivery, and §9's live case after the grants part's delivery alone |
| A foundation part's first view comes within its bound | live, after the delivery: each reader's read tool (`grants_first_views`, the mesh MCP server's `mesh_first_views`, `indexer_first_views`, `assigner_first_views`) shows, for the indexer's and the assigner's verbs, the time from its ask to the owner's answer under the two-minute bound, and no first-view condition raised by the delivery |
| The bus module's first view | cases in the nats module's one integration test: a replayed verdict older than the first view is not applied; an `indexer_bus_state` that does not answer turns the health unhealthy naming it and is asked again on the backoff; a set-aside manifest missing from the first view keeps its `asked` key, an empty `manifest-accepted` removes it; a deleted module's entry, after its empty `manifest-accepted`, is listed by `orphans` and raises `orphaned`, and a set-aside module's entry is neither; `nats_first_view_again` asks at once and records the hand act |
| `manifest-accepted` is never lost silently | unit tests of the indexer: a failed publish is published again, and one still failing writes the finding; `indexer_say_accepted` says it again for one module from the verdict held, and refuses a module with no accepted verdict |
| A person's request goes through `requests`, and the process verifies the warrant itself and acts on it once (ADR 0626 §4, ADR 0259 §6) | the process's unit tests: a `recreate` and a `drop` written to `requests` with no warrant, with a warrant for another act or entry, with a warrant the operator channel's record does not hold, and with a warrant whose ask id the process already acted on are each refused, delete nothing and say why; a `create` or the seat's `apply` for an entry no accepted manifest asks for is refused; the stamp is the caller the tool runner proved, whatever the request's body names |
| The process's account, and the bus module's only bus declaration (ADR 0626 §1 to §3) | the manifest validator's unit test: the bus module's manifest with its process's `bus` and `keeps` is accepted; `keeps` on any other process or module, any other bus declaration of the bus module, and a subject of its process on which a tool is answered are refused; the composition test: the process is granted the create, update, delete and snapshot subjects under modules' names, nothing on which a tool or a verb is answered, and no subscription to a restricted stream's subjects beyond the snapshot request, its acknowledgements and its inbox; the snapshot account is granted the same subjects as before; no tool runner and no tool of the bus module holds the bus's create, update or delete subjects |
| The process's fixes of review round 1 hold | the nats module's whole-module integration test and unit rows: an event or condition the bus did not take is kept, named by the health tool and said again at the next fill pass; a second accepted manifest asking for an entry raises no overdue condition; the warrant's wake on `decided` acts within seconds without a call to the pass by the test; a second request naming a spent ask is refused naming the act it spent; an ask past 12 hours is not acted on |
| Drift and orphans, live | live, made only by a manifest change: a manifest that shrinks a setting raises `drifted`; the conditions clear as above. Nothing is filled on the live bus, and no orphan is made there |
| Overdue and stopped are loud | live, carried by the change of the first module that moves in step 5, never by a merge made for the test alone: with the bus module's process stopped, an accepted manifest asking for an entry raises the overdue condition within five minutes, from the watchman, and a later `created` clears it; the bus module's health turns unhealthy when its pass is more than three minutes old |
| A person's tools reach the process, and only by role | live: a role without the dangerous grant is refused `drop`'s tool; the tool runner's account holds no grant of the bus's create, update or delete subjects |
| The author is the proven caller | the ledger's integration test: an `apply` naming another module's tool call is refused; the author stamped is the caller the tool runner proved, whatever the module passed |
| Only the owner reaches its data | live: another module's account reading an entry in the new form, a ledger's history, snapshot or state among them, is refused; a raw publish on a ledger's history is refused |
| The ledger keeps its promises | one integration test against the real bus (ADR 0597's How it is checked): two writers race and one is refused; two interleaved applies leave the state at the higher sequence; a write cut between history and state is repaired; a full state bucket answers taken with its sequence and catches up; an idle instance whose operations expired rebuilds from its snapshot; `rebuild`, `reindex` and `lag` repair by hand; a forged author is refused; every `apply` leaves the snapshot equal to the state; an index behind raises its condition and `reindex` clears it |
| Only the owner applies | the ledger's integration test: another module's ledger `apply` on the knowledge base's ledger is refused |
| A bare stream is complete | the manifest validator's unit test: a bare stream missing "when full", a cap or an age is refused, naming the field |
| The calls rebuild only their users | a read of the delivery plan: only the importers of the calls package rebuild when it changes |
| The move keeps data | live: a module moved from `state` keeps its bucket and data and raises no drift; a new entry in an old section is still created by the grants part; the count falls by one; at 0 a manifest using the section is refused and the foundation's are still accepted; a moved module is held off a machine whose tool runner does not read the typed binding |
**The test budget** (50 integration and 500 unit tests per repository): the design adds one integration test in the
nats module's repository and one in mesh-tools (the ledger), and table cases, not new functions, to the manifest validator's unit
tests in mesh-delivery and to mesh-sdk's decoding tests. Each repository's count against the budget is read in the
prepare step of issue 1966, and a repository over it merges tests to stay within it.
**Acceptance.** The operator asks the bus module's `list` and sees every module's kept data with its template, its
settings and its fill; a module that moved keeps its data; a drift or an orphan reaches the operator as a condition
and is changed only on their word; and the knowledge base keeps its records as a ledger (design 55 § 10).
## Milestones
Each milestone not yet delivered has its part in [25-milestones/](25-milestones/), holding every line it builds
(playbook 02, *The shape of a design*); § 1 to § 11 and the five steps of § 9 stay in this file.
- **bus-state** — § 12.2 to § 12.6, built under issue 1966: the bus module creates every module's bus state, the
seat `mesh-broker` gains its verbs and events, the grants part writes the bindings, the ledger, the calls and the
move. Its part: [25-milestones/bus-state.md](25-milestones/bus-state.md).
- **bindings-first-view** — a recovery fix under issue 1966: the grants part takes a first view of every binding
from the seat `mesh-broker`'s `list` at every start and when asked again by hand, so a binding whose `created` it
never heard is written all the same. Its part:
[25-milestones/bindings-first-view.md](25-milestones/bindings-first-view.md). Its acceptance: a module whose
bus-state entry the bus holds, and whose `created` the grants part never heard, finds the entry's binding in its
membership after the grants part's next start, with nobody's hand.
## Reviewed by architectural domain
The amendment of 2026-11-03 to ADR 0595, ADR 0596 and ADR 0597 (designs 25 § 12, 32 § 4 and 55 together) was
reviewed by the four reviewers by architectural domain side by side (playbook 01), over three rounds, and the lead
ruled on every finding. What was found, and what answers it:
| Domain | Found | Answered by |
|---|---|---|
| security | the author could be words the module chose; a person's dangerous tools had no role, mark or path to the process; the caller travelled as a plain field | design 25 § 12.3 (marks, the process's own tool subject, the identity seat's signed token), § 12.4 (the tool runner stamps the caller it proved; the move's authors marked imported) |
| seams | the bus module asked for its own events in the new form, which it would have to create itself (ADR 0595 §10); a person's tool had no join to the process; a verdict was written twice with no order | design 25 § 12.3 (its events on its old `streams` section; the process's own tool subject); design 55 § 2.1 (the `review` operation first, `reviews` caught up from it) |
| running qualities | overdue creation was undefined and raised by nobody; the database's rollback write lived in memory; a forward delivery after a rollback could miss acceptances and verdicts; no size estimate of the heads | design 25 § 12.2 (five minutes, the watchman, durable consumers, `check` at start); design 55 § 8.1 step 6 (`database.sequence`, the check at every start before the drop), § 2.1 (the heads' estimate) |
| health | the bus module's health would have fired on a quiet mesh; conditions had no clearing; `database-behind` had no clearing | design 25 § 12.2 and § 12.3 (each condition with what clears it); design 55 § 8.1 step 6 |
| events and logs | an event lost when the module stopped after `apply`; the bus module's events had no reader | design 55 § 1 (each event sent with its operation's sequence as id, the state `sent`); design 25 § 12.3 (`history` of one entry) |
| testing | the bus module had no test that starts it whole; live tests would have filled the live bus; the budget was not counted | design 25 § 12.7 (the nats module's integration test, live drift only by a manifest change, the budget paragraph); design 55 § 10 |
| upgrade | the build order lacked the grants part's binding and two undo steps | design 25 § 12.6 |
| tools | the ledger's repairs had no tool for the next owner; the bus module's events had no read tool | design 25 § 12.5 (served for every ledger owner), § 12.3 |
**Answered since, 2026-11-04** (ADR 0626, and the registry milestone's ruling on where the seat's events go): the
process's own tool subject of the security and seams rows is gone, since a process account answers no tool call; a
person's request travels through the process's state `requests` and the process verifies the warrant itself
(§ 12.3); the process holds a process account of its own beside the snapshot account (§ 12.2); and the seat's
events are on the shared stream like every seat's, with the bus module's 30-day history its own stream (§ 12.3).
**Answered since**: the margin of `full` and the events of `drop` and `recreate` were the operator's answers of
2026-11-03 and are now ADR 0595 §6's progressive insight, with the correction of fact for ADR 0595 §4 (the bus
module deletes through `recreate` as well as `drop`); both wait for the operator's Approve and are written in § 12.2
and § 12.3. The milestone's amendment (ADR 0613 to ADR 0615: the first view, the bus-state edge, the deletion paths,
the caller until the identity seat, the reader count) awaits its own review by architectural domain, which rewrites
this section.
+417 -14
View File
@@ -15,8 +15,26 @@ code:
- mesh-catalog modules/gitea/module.json
- mesh-controller internal/inventory/migrations/0063-a-machines-names-are-one-seats.sql
- mesh-controller internal/inventory/migrations/0064-the-resolver-file-is-the-uplinks.sql
updated: 2026-10-10
updated: 2026-11-09
decisions:
- "ADR 0641"
- "ADR 0636"
- "ADR 0634"
- "ADR 0635"
- "ADR 0596"
- "ADR 0593"
- "ADR 0625"
- "ADR 0573"
- "ADR 0606"
- "ADR 0609"
- "ADR 0595"
- "ADR 0627"
- "ADR 0608"
- "ADR 0613"
- "ADR 0620"
- "ADR 0612"
- "ADR 0614"
- "ADR 0605"
- "ADR 0287"
- "ADR 0223"
- "ADR 0220"
@@ -98,7 +116,7 @@ nobody argued for is an entry nobody can explain.
**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
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
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
@@ -114,7 +132,7 @@ capability is.
ADR 0110): a module
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
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.
**And the mesh's own half is data, named for its scope.** Revision, 2026-09-27, reconciling two
@@ -125,7 +143,7 @@ the set out of compiled code into a table the controller owns, so a rename is on
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
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 the fault ADR 0122 exists about.
@@ -139,32 +157,391 @@ convention, which later seats departed from.
|---|---|---|---|
| `mesh-controller` | — | mesh | — | the controller |
| `mesh-store` | — | mesh | — | the store the mesh's own records live in |
| `mesh-broker` | — | mesh | `mesh-bus` | the broker carrying the mesh's own bus |
| `mesh-broker` | — | mesh | `mesh-bus`, and `bus-state` (ADR 0595 §1, ADR 0614 §5) | the broker carrying the mesh's own bus; its holder, the bus module, creates the bus state modules ask for (design 25 § 12) |
| `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-catalog` | `the-catalogue` | mesh | — | the catalogue |
| `mesh-npm-package-registry` | `npm-package-registry` | mesh | `npm-package-registry` | the forge |
| `mesh-git` | `git` | mesh | `git` | the forge |
| `container-image-registry` | `mesh-artifact-store` for images (ADR 0605; the old name readable until every node-engine reads the new one) | mesh | `artifact-store` | the artifact registry (`distribution`) |
| `oci-artifact-registry` | `mesh-artifact-store` for bundles and archives (ADR 0605) | mesh | `artifact-store` | the artifact registry (`distribution`) |
| `mesh-catalog` | `the-catalogue` | mesh | — | the indexer |
| `npm-package-registry` | — (its rename to `mesh-*` stays deferred, ADR 0156 §8) | mesh | `npm-package-registry` | the forge |
| `go-package-registry` | — (ADR 0605) | mesh | `go-package-registry` | the forge |
| `git` | — | mesh | `git` | the forge |
| `mesh-build-machine` | `the-build-machine` | node | — | a builder |
| `mesh-resolver` | — | mesh | — | the mesh's resolvers, each holding every node's internal domain (ADR 0194); replicated, held on the anchor and the home server (ADR 0223) |
| `mesh-dns-resolver` | — | mesh | — | the mesh's resolvers, each holding every node's internal domain (ADR 0194); replicated, held on the anchor and the home server (ADR 0223) |
| ~~`mesh-dns-port`~~ | `the-dns-port` | node | — | retired by ADR 0194: the local resolver became the mesh's one; deleted from the set, and from the store's table, once nothing claimed it (ADR 0220) |
| `node-hostname` | `node-hosts-file` (renamed by ADR 0223; the former name an alias) | node | — | owns a machine's names: `/etc/hostname`, written from its holder's `hostname` setting with no default and taking effect at the next boot, and in `/etc/hosts` the machine's own lines and the operator's kept region, changed through its verbs `entries`, `add`, `remove` (ADR 0199). Held by the module `hostname`, formerly `hosts` |
| `node-resolver` | — | node | — | a machine's own resolver, held only where something requires `split-dns`: it writes `/etc/resolv.conf` there (the uplink's holder steps back from it) and routes a VPN's domains to the VPN's servers over its link through its verbs `routes`, `route`, `unroute` (ADR 0247). Held by the module `systemd-resolved` |
| `mesh-intrusion-prevention` | `the-intrusion-prevention` | node | — | an intrusion-prevention service |
| `mesh-packet-filter` | `the-packet-filter` | node | — | the packet filter |
| `mesh-private-network` | `the-private-network` | node | — | the private network the mesh runs over |
| `the-private-network` | — (it keeps its name, ADR 0636 §6) | node | — | `mesh-wireguard`, the private network the mesh runs over |
| ~~`mesh-resolver-configuration`~~ | `the-resolver-configuration` | node | — | retired by ADR 0223: `/etc/resolv.conf` is written by the holder of `node-uplink`, the program that would otherwise rewrite it, and the need of the uplink beside it (ADR 0220) goes with it; deleted from the set, and from the store's table, once nothing claims it |
| `mesh-showcase` | `the-showcase` | node | — | the showcase module |
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 —
they are read from the catalogue. **This table and
they are read from the catalog. **This table and
ADR 0126 govern, and code that
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
its reservation, and the foundation's seats delivering nothing. It is brought to this table before it
merges.
## A seat's definition (from 2026-11-03)
**A seat is defined once, in one language-neutral file, and every part learns it from the bus** (ADR 0608, from
research 081). Every seat has its definition file in its domain's contracts module, written by hand, whether it is
defined whole or at the seat list's grain (ADR 0635 §2). In the milestone of ADR 0612, 25 seats are defined whole
(ADR 0613 §1, ADR 0620, ADR 0627 §1, ADR 0634 §6, ADR 0635 §5); every other seat is defined at the seat list's grain.
- **The file** is a schema with conformance fixtures: the seat's verbs and their messages, its events and their
bodies, its states and the forms of its provisions, and names its glossary domain (ADR 0636 §5). It lives in the
contracts module of its domain (*Where a seat's
code goes*, below). Its Go and
TypeScript types are generated from it and committed beside it; nobody writes them by hand. The format is JSON, its
messages and bodies JSON Schema, as the seat list's verb inputs already are, with the format's own schema checked in;
the fixtures are data, never functions.
- **A definition names its states** (ADR 0608 §1; ADR 0634 §6 leaves the file's format to this design). A `states`
entry is what any holder of the seat keeps and what the seat's verbs answer from: its local name, its template of
ADR 0595 (state, events, work queue, log, objects or ledger), who writes it, each form of its keys with the schema
of the value under it, how long it keeps its history, and its size cap. The holder writes every state; only a state
of the events or log template may name each caller as the writer of its own keys, granted on keys under the
caller's own name and no other. No entry names a holder module, so another module can hold the seat by importing
the contract alone (ADR 0625 §7). The generated package carries, per state, its local name, a builder and a parser
per key form, and the value types, decoded strictly; the fixtures hold one value per key form.
`operator-channel`'s `records` becomes one `states` entry in the same change, so the format has one form for a
state. The format's own schema refuses an entry with an unknown template or a key form without a value, naming it.
**A state is the holder's, asked through the seat's verbs** (ADR 0596 §2): the module that reads another module's
bucket directly today keeps doing so only as a reader in transition, through its own manifest's `reads`, until its
module moves to the owner's verbs or events (ADR 0596 §3, issue 1966). The bucket's name stays
`<holder>_<state>`, the holder's own, so nothing moves under a seat's rename. **How it is checked**: the manifest
validator refuses a holder whose `requires` does not declare each of its seat's states with the same local name and
template, naming the state; the format's own schema refuses a caller-written state of another template and a state
with no size cap.
- **What is defined whole.** Ten seats came first (ADR 0613; ADR 0620, on the operator's answer of 2026-11-04 to move
them all; ADR 0627 §1): `npm-package-registry`, `go-package-registry`, `container-image-registry` and
`oci-artifact-registry` (one common part of their shared verbs, written once, so the four cannot drift),
`mesh-broker`, the four seats the milestone adds a verb or an event to, each with every verb its holder's manifest
on the trunk declares today, plus the new ones, and `node-engine`. Fifteen more are defined whole in the same
milestone because a name leaving mesh-sdk (*Where a seat's code goes*) is their type, state layout or message
(ADR 0634 §6, ADR 0635 §5), each with its verbs, events, messages and the layouts of its states:
`mesh-module-assigner`, `mesh-wants`, `mesh-node-enroller`, `mesh-catalog`, `node-build-go`, `node-build-image`,
`node-build-node`, `mesh-hand-acts`, `mesh-provisioner`, `mesh-conditions`, `mesh-self-check`, `mesh-healers`,
`mesh-watchdogs`, `the-private-network` and `mesh-delivery`. Five seats already whole gain the state layouts
mesh-sdk held for them: `node-engine`, `mesh-vault`, `mesh-delivery-checker`, `container-image-registry` and
`oci-artifact-registry` (ADR 0634 §6). **No event moves subject in this milestone** (ADR 0620), but those of the
two renamed seats (*Two seats renamed*, below): a seat's own events are only those published on its seat subject
today, and a holder's own events stay under the holder's own subjects, listed beside the seat they belong with,
never moved onto it. The four seats the milestone adds to:
- `mesh-delivery-checker` (held by the checker): the verbs `try`, `try-read` and `try-end`; no seat event. The
holder's own, beside it: its family `checker.asks`, new, with `tag-wanted` (design 51 *Tagged versions*) and
`verdict` (*Out of step is learned from events*, below); and its stream `checks`, which already declares
`preview.**` and `owed.**`, only its `emits` lacking `preview.written`, which the same change adds.
- `mesh-config` (held by mesh-config): the verbs `config` and, new, `renames`; no seat event. The holder's own,
beside it: `config-changed`, and its family `config.events`, new, with `renamed`, `retiring` and `retired`
(design 32).
- `mesh-vault` (held by the vault): the verbs `get`, `secret-ask`, `issued`, `rotate`, `move-in`, `node-key`,
`own`, `issue-account`, `sign`, `enrolment-token`, `node-account`, `rotate-node`, `node-account-confirm`,
`abandon` and, new, `retire`; no seat event. The holder's own, beside it: `provisioned`, `rotated`,
`secret-changed`, `account-hashed`, `own-replaced`, `account-issued`, `signed`, `rotation-begun`,
`node-account-ready`, `rotation-done` and, new, `own-retired` (design 32). The seat list mesh-sdk compiled
also names `accept` and `move-asks`, which no holder serves and nothing calls: the definition drops them.
- `operator-channel` (held by the router, the messenger module): the verbs `open`, `history`, `notify`, `tell`
and `asks`; the asks it accepts, `ask`, `cancel` and `condition`; its seat event `decided`; `by-caller` for
`ask`, `cancel`, `condition` and `decided`; and its state `asks`, as the holder's seat entry has them. The
holder's own, beside it: `refused`, and `offered` and `closed`, new, on its restricted stream (design 55 § 11).
Its definition is written from the holder's seat entry.
And `node-engine` (held by the node-engine on every node) has the verb `manifest-reads` and its seat event
`manifest-reads-changed`, which the milestone gives it (ADR 0618 §3, §4; design 51). The 25, by the contracts
module that holds each (ADR 0634 §7, ADR 0635 §5):
| contracts module | domain | seats | held by |
|---|---|---|---|
| `mesh-delivery-contracts` | Delivery | `mesh-catalog`, `mesh-delivery-checker`, `mesh-delivery`, `mesh-wants`, `git`, `node-build-go`, `node-build-image`, `node-build-node`, `npm-package-registry`, `go-package-registry`, `container-image-registry`, `oci-artifact-registry` | the indexer, the checker, the gate part, the assigner, the three build modules, the forge (three), `distribution` (two) |
| `mesh-health-and-repair-contracts` | Health and repair | `mesh-conditions`, `mesh-self-check`, `mesh-healers`, `mesh-watchdogs`, `mesh-hand-acts` | the condition keeper, mesh-self-check, mesh-healers, the watchman, mesh-hand-acts |
| `mesh-provisioning-contracts` | Provisioning | `mesh-provisioner` | mesh-provisioner |
| `mesh-identity-and-access-contracts` | Identity and access | `mesh-vault`, `mesh-node-enroller` | the vault, the module `mesh-enrolment` |
| `mesh-connectivity-contracts` (mesh-networking repository, ADR 0636 §6) | Connectivity | `the-private-network` | `mesh-wireguard` |
| `mesh-operator-and-conversation-contracts` | Operator and conversation | `operator-channel` | the router |
| `mesh-contracts` | Core, and Config | `node-engine`, `mesh-broker`, `mesh-config`, `mesh-module-assigner` | the node-engine, the bus module, mesh-config, the assigner |
ADR 0641 retired the Placement domain: module assignment is Core's, so `mesh-module-assigner` is defined in
`mesh-contracts`, and the wants are Delivery's, so `mesh-wants` is defined in `mesh-delivery-contracts`; the module
`mesh-placement-contracts` is not created (ADR 0641 §1, §2). From research 085's milestone (design 47) the seat
`mesh-grants` is defined whole in `mesh-identity-and-access-contracts` (ADR 0636 §5), and the seat `git` gains in
`mesh-delivery-contracts` the verbs by which every delivery part reaches the forge (*The git seat*, below). How it is
checked: the indexer refuses a definition of these seats from any other contracts module, naming both (ADR 0636 §5),
and its domain check refuses a definition naming the domain Placement (ADR 0641 §5).
**How it is checked**: each of these definitions' conformance fixtures names every event's subject, the seat's and
the holder's own, and a case of the holder's whole-module test takes a fixture from the running holder (an event
it publishes) and matches it, subject and body, against the definition; the indexer's generator tests cover each
of the 25 (ADR 0634 §10, ADR 0635 §6). The builder and the indexer's own events hold no seat:
the builder's `numbered` and `old-form-closed`, the indexer's `manifest-accepted` and its family `indexer.views`
are those modules' own, declared in their own manifests, which
ADR 0613 does not govern, and no seat definition carries them. Every other seat, the foundation's included, has its definition on the bus in the same milestone, all
at once (ADR 0608 §2), holding the seat list's grain: its name, whether it is held once or on every node, its verbs
with their inputs and its provision. Its file is written by hand in its domain's contracts module, or in
`mesh-contracts` where its domain has none; the files the registry branch generated once from mesh-sdk's seat list
are their starting text, and nothing generates them again (ADR 0635 §2). Its messages, events and state layouts are
added to a seat's definition when its domain's contract next changes, which moves that domain's seats whole into its
contracts module in that change (ADR 0613 §3); which domain moves when is tracked by issue 167 (ADR 0613 §4).
- **Two seats renamed** (ADR 0634 §9): `mesh-placement` becomes `mesh-module-assigner`, and `mesh-enrolment`
becomes `mesh-node-enroller`; the module `mesh-enrolment` keeps its name. Their definitions, the subjects of their
verbs and events, every holder's claim, every caller's `invokes` and grant, the claude-code instructions' text, the
mesh MCP server's description and the glossary take the new names in the same delivery group, and nothing answers
to the old ones: a call on an old subject fails loudly, naming the seat. **No state moves**: a state's bucket is
the holder's (`<holder>_<state>`), so placement keeps every revision. The old subjects' events keep only their
stream's own retention. **The order is the module graph's**: the grants of the new subjects are an ordering edge
that comes before the holders move, and the callers come after the holder that serves the renamed verbs; an older
node-engine takes its new build through `mesh-wants`, which is not renamed. **A machine enrolling with an installer
built before the rename** asks the old enrolment seat's subject, which no holder answers: its enrolment fails
loudly, naming the seat, and nothing joins; it enrols with an installer of the new build. **How it is checked**: the
manifest validator refuses a claim, an `invokes`, a `consumes` or a grant naming a seat no definition names, so an
old name fails
(ADR 0634 §10); the acceptance after the delivery is that `mesh-module-assigner.placement` answers and
`mesh-placement.placement` is not found, placement's revisions are those it had before, and no `seat-holders` key,
finding or condition stays under an old name.
- **Every part learns the seat list from the bus, through the seat `mesh-catalog`** (ADR 0635 §1, as ADR 0636 §4
changes it). The node-engine, the tool runner, claude-code, the manifest validator and every other reader of the
list of seats call, through mesh-sdk's runtime, the verb `seat-definitions` of the seat `mesh-catalog`: without a
seat it answers every trunk's seat with its digest, with a seat that seat's definition. They hear the seat's events
`seat-definition-changed` and `seat-definition-removed`, said for the trunks' definitions. A reader names no
module: it calls the seat, never its holder, the indexer, by name. A running part acts on what is merged, so a
reader takes no definition from a pull request's head; the manifest validator inside a delivery group's check is
the one reader that reads the members' heads. No package carries the seat list, and mesh-sdk imports no contracts
module.
**A reader without the list acts on nothing new, and keeps running what it runs** (ADR 0636 §1, changing ADR 0635
§3). A reader that starts without the list applies, composes, grants and judges nothing new, except for the
bootstrapped modules below; it keeps running what it already runs, says in its health what it waits for, and a
node-engine after a reboot starts again the compositions it last applied. Each node writes one finding when its
readers have waited five minutes (ADR 0564 §2); the condition keeper raises it as a condition, which reaches the
operator as every condition does, and clears it at the first full read. A reader away longer than the events'
retention reads the whole list again. A check that cannot read the definitions it needs fails, naming them, rather
than passing without them.
**The bootstrapped definitions** (ADR 0636 §2, changing ADR 0608 §2) are the foundation's seats (research 075) and
`mesh-catalog`, `mesh-conditions` and `operator-channel`: every reader carries them from its start and acts on their
holders without the list, so after a full restart the indexer comes back and serves the list, and the condition
keeper and the router, with the channels it sends through (ADR 0259), carry a wait's condition to the operator.
**A missing or refused definition blocks new claims only** (ADR 0636 §3): when a seat's definition is missing,
removed or refused, readers refuse a new claim, `invokes` or grant of that seat, and never stop or remove a module
that already runs holding or calling it.
**Each reader's list has two tools** in mesh-sdk's runtime bundle (ADR 0636 §7): one reads which definitions a
reader holds, their source and digests and what it waits for; the other, granted to the operator and the healers,
makes it read the list again now.
**How it is checked** (ADR 0635 §6, ADR 0636 §8; *Acceptance tests*, below): the node-engine's whole-module test,
against a stand-in of the seat, acts on nothing but the bootstrapped modules without the list and on all once it
answers, and starts its last compositions after a restart; the indexer's whole-module test starts with only the
bootstrapped seats and answers `seat-definitions`; with the indexer kept down, the node's finding is written after
five minutes and the condition keeper raises it to the operator; a refused definition stops no running module; a
reader refuses a pull request head's definition; mesh-sdk's unit test refuses a module's name in the runtime; both
tools answer.
- **The generator** is each language's build module's: the Go build module's package for Go, the Node and TypeScript
build module's for TypeScript (the logic is the module's whose work it is). The generator's version is a build input
of every contracts module, so a changed generator rebuilds each contracts module once (ADR 0603 §3). Each build
module's test step regenerates and compares, for every module carrying the contracts marker (below): a committed
file that differs, a file missing, or a file in the package tree the generator did not write fails the step, naming
each path.
- **Out of step is learned from events, and the indexer runs no generator.** For a pull request, the check's step
fails with the out-of-step paths; the checker puts them in its verdict and emits its declared event `verdict`
carrying them (its `preview.written` is declared in the same change). For a trunk, the builder's build-end event
carries a trunk build's out-of-step paths, which is how a generator's change is seen, since no check of the
contracts module runs then. The indexer consumes both and writes the trunk's finding.
- **On the bus**: the indexer reads every seat definition from every trunk and open pull request, from the forge
bridge's messages, and keeps them in its own state `seat-definitions`, keyed by seat and source (the trunk, or a
pull request's head), as their only writer; a definition is at most 256 KiB. As the holder of `mesh-catalog` it says
`seat-definition-changed` (seat, source, digest), kept seven days, only when a digest changed, and
`seat-definition-removed` when a trunk's definition goes, both as events of the seat (ADR 0636 §4, changing ADR 0608
§3). **Only the trunk's entry is the seat's contract**: the readers of the seat list read the trunks' through
`seat-definitions`; the contracts modules' builds and the review ask `indexer_seat_definition`, which answers the
trunk's unless a pull request's head is named, and says which it answered. **The manifest validator inside the
delivery group's one check reads the seat definitions at every member's head** (ADR 0636 §4), so a group that
changes a definition is judged by what it merges. A
pull request's entries are removed on the forge bridge's event that it closed or merged. **A seat of the mesh's own
contracts is defined only in its domain's contracts module, and a move there is a handover** (ADR 0636 §5, changing
ADR 0608 §2): a definition names its glossary domain; the indexer takes a seat of the mesh's
own contracts only from that domain's contracts module, `mesh-contracts` for a domain with none, and refuses a
definition of it from any other module, with a finding naming both. A definition moving from `mesh-contracts` to its
domain's contracts module is a handover: the indexer keeps the last trunk definition until the new one is merged,
in either order of the two trunks. A seat defined outside the mesh's own contracts is taken from its own contracts
module (ADR 0625 §7). A definition
that does not parse fails its pull request's check; the trunk's stays. The merge is the only approval. The
foundation's own definitions are bootstrapped before the bus exists, as the bootstrap of a fresh mesh has it
(research 075).
- **Findings**, written by the indexer, from which the condition keeper raises and clears the conditions (ADR 0564): a
trunk's seat definition that does not parse, `seat-definition-unparsed` (a pull request's is its check's verdict, not
a condition), cleared by a definition that parses; generated types on a trunk out of step with their definition,
`generated-types-out-of-step`, cleared only by a clean build end of the same contracts module, per module and not per repository, since several
contracts modules share one; a seat defined outside its domain's contracts module, naming both modules (ADR 0636 §5). **Tools** of the indexer: `indexer_seat_definition`
(read one), `indexer_seat_definitions_again` (read a trunk's or a pull request's again now) and `indexer_out_of_step`
(the generated types out of step).
- **Contracts modules.** A seat's contract is published by a contracts module, which holds the seats' definitions,
their contracts and their client packages and no seat (ADR 0625 §4, §5); the seat's holder implements the contract.
**The contracts marker**: a contracts
module's package artifact names the directory of its definitions in the field `generated-from`, and the manifest validator
refuses a module carrying it that claims a seat, naming the seat. **Each domain whose seats the milestone defines
whole has a contracts module of its own, created in the milestone** (ADR 0634 §7, ADR 0635 §5; the table above);
`mesh-contracts` keeps the Core domain's seats, `mesh-config`, and the definitions of every seat whose domain has
none yet. Each declares its packages in Go and TypeScript, cut per seat as *Where a seat's code goes* says, in
package series `0.1`, published through the two package seats (design 51) and taken by its users by version, or
inside one delivery group by the group's head.
- **A contracts module and a domain SDK are modules like any other, found by their manifests** (ADR 0634 §8).
`mesh-delivery-contracts` and the Delivery domain's SDK are folders of the mesh-delivery repository, and the other
domains' contracts modules folders of the mesh-contracts repository, but the Connectivity domain's:
`mesh-connectivity-contracts` is a folder of the mesh-networking repository, beside `mesh-wireguard`, which holds
`the-private-network` with its manifest and peer generator its own (ADR 0636 §6, changing ADR 0634 §8). Each is a package-only module, as mesh-sdk long
is: published from its trunk, rebuilding the modules that use it like any package (ADR 0573 §2, §3), and delivered
to no node. A step of the build or the delivery that treats a module in a folder unlike one at a repository's root
is a gap of the build and delivery system, fixed in the milestone, never worked around. **How it is checked**: the
delivery group's one check builds and publishes every new module from its folder, and the build modules'
regenerate-and-compare step runs on every contracts module the milestone creates (ADR 0634 §10).
- **mesh-sdk takes no seat-specific type** (ADR 0613 §5): a unit test of mesh-sdk refuses an exported identifier or
string constant naming a seat the definitions define. The two checks every change already runs, the
build modules' compare step and that test, are where a stray file and a stray type are refused; mesh-catalog's own
checks cannot see another repository.
- **A contracts cycle holds nothing.** `mesh-delivery-contracts` is published through the forge module's package seats
and built by the Go and Node build modules, and the forge module, `distribution` and the Go build module import it. A
package edge and a build module's edge are triggers, never waits, so that cycle starts and is not refused (ADR 0614
§6), and a package is numbered per source revision, so the triggers end (design 51 *The versions*).
- **Older readers keep working**: a moved contract keeps its subjects, keys and message shapes, and a reader ignores
a field it does not know. The two renamed seats are the exception, and every one of their users moves in the same
delivery group (ADR 0634 §9, as it changes ADR 0608 §6).
- **A vendor-specific** provision or contribution lives in its module's own extension package, published by version
like any package; a later holder of the seat may publish its own the same way. A seat's definition stays
vendor-neutral, which the seams review checks.
### Where a seat's code goes
*Added 2026-11-04, widened 2026-11-05* (ADR 0625, ADR 0627, ADR 0634, ADR 0635; the operator, 2026-11-04: "many small
sdk's instead of 1 large sdk", and shared logic lives with its owner). A module is rebuilt only for what it uses, so
shared code lives with its owner: a seat's code is cut per seat, in the contracts module of its domain; what one
module owns is that module's own package; what one domain's modules and seats share is that domain's SDK; mesh-sdk
keeps only what modules or seats of more than one domain use.
```
contracts module of a domain (mesh-<domain>-contracts; mesh-contracts for Core, mesh-config, and a domain with none)
├── definitions and fixtures written by hand, every seat's at its grain, the generation's source
└── one package per seat defined whole, per language generated: types, subjects, state layouts, a typed call per verb
└── its client package (where callers or holders share logic over the seat) written by hand
domain SDK (mesh-<domain>-sdk) written by hand: logic and forms one domain's modules and seats share, owned by no module
a module's own package written by hand: logic and state layouts one module owns, where it holds no seat for them
mesh-sdk the runtime every module uses, and what modules or seats of more than one domain share;
it imports no contracts module and reads the seat list from the bus
```
- **Shared code lives with its owner** (ADR 0634 §1), and the first home that fits is the one: a seat's types, the
layouts of its states and the messages of its verbs and events are its contract's, and the logic its callers or
holders share is its client's; logic and state layouts one module owns, where that module holds no seat for them,
are that module's own package, which other modules import from it; logic and forms no seat carries and no one
module owns, the seat-neutral parts seats' clients or holders share included, are their domain SDK's when only one
domain's modules or seats use them, and mesh-sdk's when more than one domain's do or every module does.
- **Each seat defined whole has a package of its own, per language** (ADR 0625 §1, ADR 0627 §1): its generated
contract, and a module imports only the packages of the seats it uses, so a change of that seat rebuilds only its
users. Where the fingerprint counts a language's module rather than its package, each such package is a module of
its own in the same folder. In the milestone of ADR 0612 these are the 25 seats above.
- **A seat at the seat list's grain has its definition and no package** (ADR 0635 §1, §2): its readers learn it
from the bus, and a change of it rebuilds nothing.
- **The imports run one way per package** (ADR 0625 §3, as ADR 0634 §2 and ADR 0635 §1 change it): a generated
package imports nothing of mesh-sdk; mesh-sdk imports no contracts module; a domain SDK imports only mesh-sdk and
contract packages; a client imports its own seat's contract, mesh-sdk's runtime and its own domain's SDK. Where a
language's module is the unit of import, the clients are a module of their own in the folder, so the two never
form a cycle.
- **A client package** (ADR 0625 §4, as ADR 0634 §2 changes it) is the one hand-written package a seat may carry
beside its contract, owned by the seat's contracts module, never by a holder. It holds the logic two or more
callers or holders would otherwise each write over that seat, and only where such logic exists; it uses only its
own seat's contract, mesh-sdk's runtime and its own domain's SDK, never another seat's client, and never names a
holder, a module's tool or a module's subject. A seat's types and typed calls are its contract's, never its client's.
- **A domain SDK** (ADR 0634 §2) is a package-only module, `mesh-<domain>-sdk`, named after the domain's heading in
the glossary, which may sit in a repository with that domain's modules. Its importers are its domain's modules and
the holders and callers of its domain's seats; it imports only mesh-sdk and contract packages, names no module's
tool or subject, and holds no seat's type or state layout and no state layout one module owns. The first is
`mesh-delivery-sdk`, in the milestone of ADR 0612: the moving of a file in parts (below), the build steps' and the
composer's shared logic, each made seat-neutral over its callers' types (ADR 0635 §4), and the rules the Delivery
domain's modules share.
- **A module's own package** (ADR 0634 §1) holds what that module owns and no seat carries: its state layouts that
other modules read and the logic over them. Its fingerprint counts only that package's files (ADR 0593 §2), so a
change elsewhere in the owner module rebuilds none of the package's importers. Another module of the same repository uses it by path, from the same
commit (ADR 0609 §2); a module of another repository uses it only by its name and a published version (ADR 0573
§1), so the owner's manifest declares it as a package artifact (ADR 0606 §4).
- **The clients of the milestone of ADR 0612** (ADR 0627 §1, ADR 0634 §4, ADR 0635 §4), each moved out of mesh-sdk
with every importer in the same delivery group, mesh-sdk keeping no copy and no alias: `operator-channel` (an ask
and its awaited answer, mesh-sdk's `asks` and `asker`); `mesh-vault` (the calls its callers share; mesh-sdk's
hand-written vault types are replaced by the vault contract's, not moved into the client); `node-engine` (the
checks of its compose-candidate ask and answer); `npm-package-registry`, `go-package-registry`,
`container-image-registry` and `oci-artifact-registry` (publishing a file in parts, one client per seat, each
binding the Delivery domain's SDK's transfer to its own seat's verbs and types); `mesh-hand-acts` (recording a
hand act); and `mesh-provisioner` (mesh-sdk's provider runtime and its harness, which only providers use).
`the-private-network` has none: the layout of the value it provides, which holds no key material, is the seat's
contract in `mesh-connectivity-contracts` (ADR 0634 §1), the private network's manifest and peer generator are
`mesh-wireguard`'s own, and the seat has a client only where its callers share logic (ADR 0636 §6, changing ADR 0635
§4).
- **Moving a file in parts is seat-neutral, in the Delivery domain's SDK, on both sides** (ADR 0627 §2, as ADR 0634
§3 changes it): it serves only the four registry seats, all of the Delivery domain. The sending side and the going
on from what a holder already holds, and the holder's side (taking the parts into the holder's own objects,
checking their hashes, dropping an upload left idle, saying when its objects fill), name no seat, verb or seat's
type and take subjects, shapes and checks from their caller. Each registry seat's client binds the sending side,
and each holder (the distribution module, the forge's two package seats) binds the receiving side to the seat it
holds, so no holder copies another's transfer.
- **In a contracts module's published packages every file outside a client is generated** (ADR 0625 §5): the build
modules' regenerate-and-compare step fails any other hand-written file there, naming it. The definitions and their
fixtures sit outside the published packages and are not judged by that step.
- **The contracts modules follow the glossary's domains** (ADR 0625 §6, ADR 0634 §7): a domain's seats move into
`mesh-<domain>-contracts`, named after the domain's heading in the glossary, when that domain's seats move; a seat
of the Core domain, or of a domain with no contracts module yet, is in `mesh-contracts`. The seven the milestone of
ADR 0612 creates are in *A seat's definition*, above; the order of the other domains stays issue 167's (ADR 0613 §4).
- **A seat defined outside the mesh's own contracts has a contracts module of its own** (ADR 0625 §7), which may
share a repository with its first holder; its definition, contract and client never live in the holder module, so
another module can hold the seat by importing only the contract, and the holder can be retired while the contract
stays.
- **A role only one module could ever play is no seat** (ADR 0625 §8): it is that module's own tools and events,
reached by the module's name (ADR 0596 §2). The test is whether another module could take the role over, not how
many hold it today, so every seat in force stays a seat, `mesh-broker`, `node-engine` and `route-proxy` included.
- **mesh-sdk takes only what modules of more than one domain use** (ADR 0625 §9, as ADR 0627 §2, ADR 0634 §1, §4, §5
and ADR 0635 §4 change it): the bus connection, the tools bundle's runtime, state and its helpers, the answer of a
module's health tool, the `Membership` type with the names its runtime reads to learn its own membership, the
package `views` (which takes its hand-act recorder and its finding writer from its caller), `outward`, the parts of
`forgekey` that modules of more than one domain use, and a seat-neutral part seats of more than one domain share,
as long as it names no seat, verb or seat's type. In the milestone of ADR 0612 its `go/delivery` is moved whole and
deleted, each of its names to the home this section gives it, and every other part naming a seat or a holder's
state leaves for its owner, every importer moving in the same delivery group; after it, mesh-sdk holds no package,
type or function over a seat defined whole, and no seat list (ADR 0627 §4, ADR 0634 §4, ADR 0635 §1).
**How it is checked** (ADR 0625 §11, ADR 0627 §3, ADR 0634 §10 and ADR 0635 §6, all built in the milestone of ADR 0612):
| Rule | Checked by |
|---|---|
| One package per seat defined whole | a unit test of the generator (one program in the Go build module, emitting both languages and run by the Node build module too) emitting one package per seat defined whole, covering each of the 25; the manifest validator refuses a contracts module whose package holds the generated files of two seats defined whole, naming them |
| The imports run one way | a mesh-sdk unit test that no package of mesh-sdk imports a contracts module; the generator tests check no generated package imports mesh-sdk; a domain SDK's importers and imports by the review below |
| mesh-sdk holds no logic over seats, and no part that left it | mesh-sdk's package-list test refuses by name `go/delivery`, `asks`, `asker`, the vault's types and its shared calls, the compose-candidate checks, the provider runtime, and every other package or part ADR 0634 §4 and ADR 0635 §4 move; its seat-naming test refuses the defined seats' names in mesh-sdk, so a copy kept without importing a contract fails too |
| A client uses only its own seat's contract and its domain's SDK | the catalog check of a client: its own test refuses a client importing another seat's client or another domain's SDK, and one naming a module's tool or subject, and passes every client of the milestone |
| The parts transfer is seat-neutral | its unit test, in the Delivery domain's SDK, runs both sides against two different subjects and shapes the caller gives, and the seat-name check holds it seat-neutral; the review refuses a holder that takes parts in without it |
| Only a client is written by hand in a published package | each build module's regenerate-and-compare test fails on a hand-written file outside a client package and passes a contracts module holding its definitions and fixtures; the step runs on every contracts module |
| A module in a folder is built and published as one at a root | the delivery group's one check builds and publishes every new contracts module and the domain SDK from its folder, and `mesh-wireguard` and `mesh-connectivity-contracts` from the mesh-networking repository |
| A seat is defined only in its domain's contracts module | the indexer refuses a second module's definition of a seat, with its finding naming both, and a move from `mesh-contracts` holds in either merge order (ADR 0636 §5) |
| A holder keeps its seat's states | the manifest validator refuses a holder whose `requires` does not declare each of its seat's states with the same local name and template |
| A contracts module holds no seat; a role only one module could ever play has none | the manifest validator's refusal of a contracts module that claims a seat; review both ways: a new seat definition's pull request is asked whether another module could take the role over, and a module's pull request is asked the same of each new tool or event family other modules call |
| Each new exported name sits where this section puts it | the review of every pull request (playbooks 07 and 08, the code reviewer's definition) asks the home of each new exported type, field, constant, package or module against this section, a domain SDK's importers included |
**Acceptance tests**, each at its level (unit, or a case of the module's one whole-module integration test against the
real bus and store) and in its repository. The move raises no repository's test count, since mesh-delivery is already
over its budget: each case below replaces or joins an existing one.
| Test | Level | Repository |
|---|---|---|
| research 081's 14, 21, 25 and 26 | as research 081 sets them | as research 081 sets them |
| mesh-sdk's seat-naming, import and package-list tests; no module's name in the runtime | unit | mesh-sdk |
| a reader of the seat list, against a bus the test gives: a reader away longer than the events' retention reads the whole list again; a reader refuses a pull request head's definition; both list tools answer, the re-read one only for the operator and the healers | unit | mesh-sdk |
| the node-engine, against a stand-in of the seat `mesh-catalog`: without the list it acts on nothing but the bootstrapped modules, then on all once the stand-in answers; after a restart it starts its last applied compositions again; a refused definition stops no running module | a case of the node-engine's whole-module test | `mesh-host` (the node-engine's repository) |
| the indexer starts with only the bootstrapped seats and answers `seat-definitions`, without a seat (names and digests) and with one; with the indexer kept down, the node's finding is written after five minutes and the condition keeper raises it to the operator | cases of the indexer's whole-module test | mesh-delivery |
| a trunk definition that does not parse writes its finding and a fixed one clears it; a trunk build end naming out-of-step types writes the finding, a clean build end of another contracts module of the same repository leaves it, and a clean one of the same module clears it | a case of the indexer's whole-module test | mesh-delivery |
| `indexer_seat_definition` answers the trunk's entry while a pull request holds another; a pull request's entries go when it closes; a definition over 256 KiB is refused; a seat-list-grain definition is read from a contracts module like a whole one; a removed definition says `seat-definition-removed` and its seat takes no new claim afterwards; a second module defining a seat is refused with its finding, and a definition moving from `mesh-contracts` to its domain's contracts module holds in either merge order; each of the indexer's tools answers | cases of the indexer's whole-module test | mesh-delivery |
| the manifest validator refuses a claim, an `invokes`, a `consumes` or a grant naming `mesh-placement` or the seat `mesh-enrolment`, and a holder missing one of its seat's states in `requires` | unit | mesh-delivery |
| the checker's `verdict` names out-of-step files; the group's check reads seat definitions at every member's head | a case of the checker's whole-module test | mesh-delivery |
| each build module's compare step fails on a differing, a missing and an unwritten file, naming each, per contracts module | unit | mesh-catalog |
| a change in one folder module rebuilds and publishes only it and its importers; a module with both a deployed and a package artifact (the grants part or disk-load) is built, delivered and published | a case of the Go build module's whole-module test | mesh-catalog |
| after the delivery, `mesh-module-assigner.placement` answers and `mesh-placement.placement` is not found; placement keeps its revisions; no `seat-holders` key, finding or condition stays under an old name | live, read through the mesh's tools | the mesh |
| `mesh-wireguard` is built and delivered from the mesh-networking repository | the group's one check, then live | mesh-networking |
## The foundation's seats
`mesh-controller`, `mesh-store` and `mesh-broker` name which assignment the mesh *itself* uses: the
@@ -312,6 +689,13 @@ difference: it receives a URL either way.
With the seat unheld, a build from the seat is refused and says why. External builds carry on.
**The seat's verbs, from research 085's milestone.** The delivery parts reach the forge through the seat `git`'s
verbs alone, never by the forge module's own tools: reads for the parts that declare them in their `invokes`, and
acts (a status, a comment, the delivery's view, a line on the commit's note, a merge) for the forge bridge alone, as
the holder's trusted value `callers` names; the forge's events (a pull request updated, merged or closed, the forge's
own webhooks, a repository writable again) are the seat's events. Design 47, *The verbs*, lists them with their
callers and how they are checked.
**Not yet designed:** a credential for cloning a private repository. The mesh's own repositories are
public. The natural place for a clone credential is a `secret` from the vault, and that is a decision
still to take.
@@ -326,7 +710,7 @@ checked as their tables say:
| 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. |
| `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. |
@@ -339,7 +723,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*. |
| 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 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 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. |
@@ -347,3 +731,22 @@ checked as their tables say:
| The uplink's step-back comes from the seat | 0287: the composition tests for the uplink and the resolver on one machine pass with the case in the collision check removed. |
| Holdings are derived, and the overview lists every seat | 0118: the `seats` command test, including an unheld seat. |
| A build source on the seat records no address; an unheld seat refuses only self-hosted builds | 0111's tests. |
## Reviewed by architectural domain
The amendment of 2026-11-03 for research 081 (ADR 0605 to 0611; *A seat's definition*) was reviewed with designs 51
and 32 by the four reviewers by architectural domain; the findings and what answers them are listed in design 51's
*Reviewed by architectural domain*. For this design: only the trunk's definition is the contract (security), the
indexer learns out-of-step types from the check's verdict event and runs no generator (seams), findings go to the
condition keeper (health), a pull request's entries are removed when it closes (running qualities).
The milestone's amendment (ADR 0612, ADR 0613, ADR 0614, ADR 0620: the seats defined whole, the rest at the seat
list's grain, the contracts marker, out of step learned from the checker's `verdict` and the builder's build end, the
seat list's one source), with the amendment of 2026-11-04 (ADR 0625, ADR 0627: ten seats defined whole, a package per
seat, a package for the seats at the seat list's grain in `mesh-contracts`, the seven clients and the seat-neutral parts transfer, *Where a
seat's code goes*), and the amendment of 2026-11-05 (ADR 0634, ADR 0635: shared code with its owner, the domain
SDK, a module's own package, a contracts module per domain the milestone moves, 25 seats defined whole with their
states, the seat list read from the bus, two seats renamed), with ADR 0636 (the seat list through the seat
`mesh-catalog`, a reader without it acting on nothing new, a seat defined only in its domain's contracts module, the
Connectivity domain in the mesh-networking repository; accepted at its version 15) and the lead's rulings on the first review by architectural
domain written in, await their final review, which rewrites this section.
@@ -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
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
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
@@ -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.
*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)).
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)),
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
`${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.
*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
backend that keeps it (ADR 0049):
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
(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
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
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)).
@@ -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 |
| 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
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
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.
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
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
@@ -464,7 +464,7 @@ Each phase ends at a check that holds, so none of them leaves a mechanism half-r
| Rule | Checked by |
|---|---|
| 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 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. |
@@ -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. |
| 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. |
| 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. |
| 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
+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
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.
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
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.
**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
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
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
quietly.
- [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
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
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
`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
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
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
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
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 —
**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
@@ -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
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
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
@@ -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 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
the foundation, because those are the things the catalogue needed in order to exist
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 catalog needed in order to exist
([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
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 |
| 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 |
| 4 | the catalogue | 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 |
| 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 catalog makes every unconverted module unregisterable |
| 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
@@ -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,
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
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
breaks" on record (ADR 0130).
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
> shutting it down ends the path that reaches this installation's machines from a workstation.
@@ -5,7 +5,7 @@ code:
- mesh-controller internal/inventory
- mesh-controller internal/catalogue
- mesh-controller cmd/mesh-controller
updated: 2026-10-16
updated: 2026-11-06
decisions:
- "ADR 0181"
- "ADR 0182"
@@ -16,6 +16,11 @@ decisions:
- "ADR 0117"
- "ADR 0399"
- "ADR 0400"
- "ADR 0412"
- "ADR 0415"
- "ADR 0416"
- "ADR 0417"
- "ADR 0422"
---
# 29 — A node has operator accounts, and the mesh owns what lives under a home
@@ -58,6 +63,15 @@ These are a **family**, not one module: an `ssh-client` module, a shell module,
module, each a *universal-tier* consumer of the account fact — assigned wherever a person logs in,
which is every node, unlike the graphical stack that a capability gates.
**A folder a module owns says how it treats a file it did not write** (ADR 0416, ADR 0422). Declaring a
directory under a home owns it (ADR 0182 §1), and the owner alone writes there and reacts to every file
that enters it, never losing what the file held. The manifest says how, on the directory itself:
`unknown` is `move-out` (the node-engine moves every path the manifest does not declare and `knows` does
not name into `~/.local/state/mesh-moved/<module>/`, with a list), `module` (the module's own process
reacts, as ssh-client does in `~/.ssh`) or `program` (everything in it is the program's, as in a
service's data directory). `move-out` is only for a folder under a home, the home of the account the
assignment runs for (ADR 0412 §5). A folder many modules write in is not declared at all.
## 3. The whole of `~/.ssh` is the mesh's — with one boundary drawn inside it
The predecessor owned a single file (`~/.ssh/config`) and left the rest alone; it drifted, because
@@ -84,6 +98,9 @@ ADR 0117 draws for the uplink and the sshd
module draws for the firewall: **the mesh must never be able to arrange the one failure that severs
its own way back in.** The carve-out is not a convenience; it is that rule, in `~/.ssh`.
Since ADR 0415 and ADR 0417 the found class no longer holds in `~/.ssh`: ssh-client moves out what it
does not know into a kept folder, and never moves a key or `authorized_keys`, which keeps this boundary.
## 4. Keys are the mesh's to generate — through a CA, and existing keys are adopted, not replaced
Key *generation* is the mesh's, not each node's improvising its own. The clean form is an **SSH
@@ -157,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
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
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)
@@ -192,7 +209,7 @@ alias and its trust, and a fresh machine has no operator dotfiles at all — the
service and leave the human unable to work on the box.
**Why not build it reflexively:** it is a real addition to the node model, the resource model, and
the seat set, and must be gotten right. The mechanism half is now settled — ADR 0120 is what lets
the seat list, and must be gotten right. The mechanism half is now settled — ADR 0120 is what lets
the ssh files be templates with no controller format — so what remains to decide here is the
model:
@@ -1,8 +1,8 @@
---
layer: to-be
status: proposed
code: []
updated: 2026-10-06
status: in-progress
code: [mesh-controller]
updated: 2026-11-06
decisions:
- "ADR 0238"
- "ADR 0221"
@@ -15,6 +15,8 @@ decisions:
# 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, 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
change freezes it.** A code change lands in `mesh-controller` or `mesh-catalog`, and getting it onto
the machines is a sequence somebody types. The predecessor's pipelines rebuilt and redeployed on a
@@ -28,7 +30,7 @@ webhook".
seat or schema:
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
records the new image.
3. The mesh **reconciles on its own**: the module's declaration now names the new image, the next
@@ -39,7 +41,7 @@ seat or schema:
**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:
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
still-old-shape stored manifests, and composition freezes for every node that runs an affected
module.** Running services are untouched; only new declarations stop.
@@ -61,7 +63,7 @@ automating this must solve.
### 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`**
(`~/.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
@@ -76,12 +78,12 @@ HAL service, and:
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
`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.
### 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*
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
@@ -102,7 +104,7 @@ does not know. The old builder cannot build the new builder. Escapes:
Either way, self-update for breaking changes needs a **transition discipline** so a push does not
auto-freeze: the new controller (and builder) should accept the *old and new* shape together for
one release — deprecated aliases in the seat set, a schema that reads both — then a later release
one release — deprecated aliases in the seat list, a schema that reads both — then a later release
drops the old. With that, a breaking change rolls out on a push like any other: everything reads
both, the manifests migrate, the compatibility is removed. Without it, self-update would simply
automate the freeze.
@@ -124,7 +126,7 @@ automate the freeze.
Revision, ADR 0162. The
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
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
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
@@ -166,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
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;
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.
## What a push does not send (2026-10-05)
+225 -12
View File
@@ -12,8 +12,16 @@ code:
- mesh-tools src/main.ts
- mesh-catalog modules/mesh-catalog
- mesh-tools node-tools/internal/runtime (a module's state, ADR 0201)
updated: 2026-10-12
- mesh-sdk (the typed bus-state entry in `requires` and its binding, ADR 0595)
updated: 2026-11-04
decisions:
- "ADR 0609"
- "ADR 0612"
- "ADR 0613"
- "ADR 0618"
- "ADR 0606"
- "ADR 0595"
- "ADR 0596"
- "ADR 0307"
- "ADR 0240"
- "ADR 0233"
@@ -62,7 +70,7 @@ onto this in step 3 of ADR 0116.
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
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 |
|---|---|
@@ -94,7 +102,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
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
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
@@ -117,7 +125,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.
**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
declarations.
@@ -172,7 +180,7 @@ can be wrong.
**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
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
no permission to publish into an asker's inbox, which is the one grant design 25 §4 refuses by name.
@@ -227,7 +235,7 @@ service" versus "one worker per machine".
| credential | sealed, per consumer | none | none | none | none |
| reply | — | none | none, or an event later | a report | awaited |
| retention | — | age and size | work queue, explicit ack | **last per subject** | none |
| declared | `provides`/`requires` | `emits`/`consumes` | seat `accepts` / `uses` | the mesh's own; a module's `state` / `reads` | `serves` |
| declared | `provides`/`requires` | `emits`/`consumes` | seat `accepts` / `uses` | the mesh's own; a module's bus state in `requires` (ADR 0595), the old `state` / `reads` until it moves | `serves` |
**Job** is the one ADR 0041 had no room
for. Its table has events at 1:many and provisions at 1:1; a module submitting work to a service
@@ -249,7 +257,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
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
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
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"
@@ -278,6 +286,42 @@ module's assignment** — what a module stored is data
(ADR 0030) — and one whose
declaration is gone is reported, never removed by the mesh.
**A module asks for its bus state in `requires`, and reads no other module's.** *Amended 2026-11-03, ADR 0595
and ADR 0596, from research 082.* The `state` and `reads` of the two paragraphs above, and the sections `logs`,
`streams` and `objects` beside them, are four vocabularies for one need: what a module keeps on the bus. They are
replaced by one form, a **provision** of the bus module, as a database is postgres's:
- **An entry of `requires`, in its typed long form** (ADR 0595 §1): the local name, the storage type, the template
(state, events, work queue, log, objects, ledger) and the template's options, within their bounds; a ledger also
names its type (its schema, its operations each with its ledger effect, its indexes). The templates, their fixed
settings and their options are [design 25](25-the-bus-on-nats.md) § 12.1. mesh-sdk's catalog package carries the
entry's type and its strict decoding; every rule that judges it lives in the manifest validator (ADR 0595 §8).
- **The binding is what the module receives**: the names on the bus for each local name, written into its
membership by the grants part once the bus module says `created` (design 25 § 12.2). The module still names only
locally (§ 1). A module whose binding is missing at start fails loudly, naming the entry.
- **One name, one form.** A module asks for each entry once: an old section and the new form are never both used for
one local name, and one name serves one storage type only.
- **No readers.** The new form has no `reads`, and the manifest validator refuses one (ADR 0596 §2). Only the owner's account
reaches its bus state. **A module's events are subscribed to; its data is asked for**: what another module needs
of a module's kept data it asks through that module's tools or seat verbs (§ 4's tool relationship, `serves`),
and what a module lets others hear is its declared events, heard through `consumes` (ADR 0363, unchanged; ADR 0596
§1). A bucket's layout is never a contract another module depends on; a tool or a declared event is.
- **A state may be read-only to the module's tools.** A state's declaration may carry `read-only: true`: only the
processes of the module whose `bus.state` names that state write it, and the module's tools read it, holding no
publish on its subjects. The manifest validator refuses a read-only state that no process of the module names. How it is checked:
the manifest validator's unit tests for the refusal, and the composition test that a tools bundle's account holds a read on the
state and no write. The bus module's state `spent` (design 25 § 12.3) is the first user. (A design limit of the
milestone's review; no record decides it yet.)
- **The move is a module at a time** (ADR 0595 §11, ADR 0596 §3), each when it is next touched, keeping its local
names and its old settings as options so its data stays. A reader moves after its owner: the owner declares the
event or gains the tool its readers need first, and the manifest validator refuses a moved module's subscription to an event
its owner does not declare. An old section is refused once no module but the foundation's uses it.
What follows for the paragraphs above: for a module that has moved, ADR 0201 §2 (another module's state read by
naming it in `reads`) no longer holds, and the bucket "every instance of the owner writes; a reader reads and
watches" becomes "every instance of the owner writes and reads". The runtime's `get`, `put`, `delete`, `keys`,
`watch` and `history` stay the calls of the template state.
**A module declares the data it holds.** *Added 2026-10-06,
ADR 0233.*
State on the bus is one kind of data; the rest lives on a machine's disks or with a provider, and the
@@ -326,7 +370,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
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
(ADR 0069), and submits one
**job** per affected module to the `mesh-build-machine` seat. The builder stays simple: it builds
@@ -366,7 +410,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,
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
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
produces none, so absence is not health.
@@ -437,6 +481,136 @@ meaning into shape wherever it can go — a required `channel` field is caught,
interpretation of an existing one is not. Saying so is better than implying the fingerprint is
complete, because a team that believes it is complete stops reviewing for the case it misses.
### A package, its series, and the series its users accept (from 2026-11-03)
(ADR 0606 §4, ADR 0609, from research 081.) This is the manifest's half of design 51's versions.
- **A package is declared as an artifact**: its name, the kind `package`, its language (`go` or `typescript`), its
paths and its **package series** (`0.1`). An image, a bundle or an archive may declare a package series the same
way; one that declares none is numbered in `0.1`. Nobody types a version: the builder counts it (design 51).
- **A package is named by its name in its own language** (a design limit of the registry milestone, 2026-11-04): a
Go package by the module path of its `go.mod`, a TypeScript package by the name in its `package.json`. The
indexer reads that name at each merge, the builder numbers the package under it, the package's registry seat
publishes it under it, and a user's `packages` names it so. One name per package across the mesh, because the
language's own tools fetch by that name and no other. How it is checked: the manifest validator refuses a name in `packages`
that no package of the shelf is named, or that a member of the change's delivery group does not name at its head.
- **Raising a package series** is a reviewed change of the package's manifest, for a change that breaks its users.
The checker's verdict names every change of a package series it sees, so the review of the change sees it.
- **A user names the mesh packages it takes in a section of its own, `packages`**, each with the package series it
accepts. It is not `uses`, which grants publishing on a seat's `accepts` subjects (§ 3); a package is not a seat. A
user builds, and is checked, against the newest version of that series; a publish in another series does not move
it, and moving is a reviewed change of the user's manifest. A user that names none accepts the series of the
package's first numbered version, so nothing is edited in today's users. The indexer's tool `indexer_package_users`
lists, per package, its users and the series each accepts, so a user left on an older series is seen.
- **Inside one repository** a module uses its sibling package by path, from the same commit, and waits for no publish.
- **A user outside the package's delivery group is not built before the merge**: it is built after it, when the new
version is in its series. A user that breaks fails its own build, is not wanted, keeps its previous build, and a
condition tells the operator. A group's members are built in the group's check, against the group's tagged version.
- **A contracts module's package** names the directory of the seat definitions it is generated from in the field
`generated-from`, the contracts marker (design 26).
- **The manifest validator reads the new words** (ADR 0578 §1): each package series well formed and only on the four kinds that may
carry it, a package naming `go` or `typescript`, and each name in `packages` a package some manifest declares. A
running manifest validator that does not know a word sets the manifest aside to wait, never refuses it, while a newer indexer
build is merged and not yet delivered; and a node-engine is handed a manifest using these words only once it
states the capability to read them (ADR 0614 §4).
- **Cost**: the new manifest fields are read through mesh-sdk's manifest types, so their change rebuilds every module
once; it is the one rebuild the milestone of ADR 0612 already takes for the contracts move (ADR 0612 §1, ADR 0613 §1).
### A config value renamed (from 2026-11-03)
(ADR 0609 §4.) A config value's declaration may name its old name, `renamed-from`. On the merge, mesh-config moves the
value it holds under the old name to the new name in its own state, without the operator giving it again, emits
`renamed` (module, old name, new name, when; its family `config.events`, kept seven days) and records it. This design decides what ADR 0609 leaves open:
- **Which values**: a module's own config values only, never a secret, never another module's value. A rename keeps
the value's trust: a value set without the operator's Approve stays untrusted under its new name (ADR 0348 §8), and
the checker refuses a rename that would change a value's trust.
- **How long the old name is read** (the **rename window**): until every node-engine whose node has a want of the
module reports applied a build of it that reads the new name. mesh-config hears the status view's `delivered`
events and, at its start and on each, asks its `node_haves` for every node, away ones included, since `delivered`
counts only the nodes present (design 47); until then mesh-config answers both names. A node away when the last
`delivered` was said should close its window when it returns, with no other delivery: mesh-config also wakes on
the node-engine seat's event `mesh.seat.node-engine.event.manifest-reads-changed`, said when a node's stated
capabilities change and when it returns after being away (ADR 0618 §4), as for the old seat name (design 51), and
on it reads that node's stated capabilities through the seat's verb `manifest-reads` (ADR 0618 §3) and its applied
builds through `node_haves`, never a watch of the node-engine's have state (ADR 0596). mesh-config declares
`seat:node-engine.manifest-reads` in its `invokes`: that is the grant the grants part composes, and with it the
grants part grants mesh-config the subscription to the event's subject (its unit row is in design 51). A
node-engine that does not yet serve `manifest-reads` counts as stating nothing: the window stays open and
the seat's verb `renames` names the node. ADR 0618 does not cover a
returned node that applies the module's new build only after its return event, since that build changes what the
module reads, not what the node-engine states: such a node closes the window on the next `delivered`, whichever
delivery says it. A node holding a window open
for seven days is mesh-config's finding naming the node, cleared when the window closes; `config_rename_close`
drops a node from a window by hand, recorded as a hand act.
- **The checker asks the seat, never the module.** The seat `mesh-config` gains the optional verb `renames` (the
open renames, and the nodes holding each window open), in its seat definition (design 26). The checker asks it for
every change carrying `renamed-from` or dropping one: a holder that does not serve it is refused by the tool runner
naming the verb, and the checker then refuses `renamed-from` naming that; while a window is open, the same answer
refuses dropping `renamed-from`, naming the nodes. A check that cannot ask fails, and says so. So no manifest uses
`renamed-from` in the milestone that brings it, whose running mesh-config serves no `renames`; the first that does
is its live case. The checker declares `seat:mesh-config.renames` in its `invokes`, so by the served-verb edge
(ADR 0614 §2) its new build waits until a build of mesh-config that serves `renames` runs healthy; no cycle closes,
since mesh-config waits only on the indexer's `indexer_config_readers`. The "not served" answer is then met only by
the running checker in the milestone's own check, and after a revert. `config_renames` names, per rename, the nodes
still holding the old name open.
- **A refused move** (the old name holds nothing, or a secret) is mesh-config's finding, from which the condition keeper
raises the condition, cleared once the value moved or `renamed-from` left the trunk's manifest. **Tools of
mesh-config**: `config_renames` (what moved, from which name, when), `config_rename_again` (move one value by hand
when a move was refused) and `config_rename_close` (above).
### A value no longer read (from 2026-11-03)
A config value or secret no manifest on any trunk reads any more is a **retiring value**. mesh-config learns it from
the indexer's tool `indexer_config_readers`, which it asks on each of the indexer's `manifest-accepted` events; the
indexer says `manifest-accepted` for every newly accepted manifest, its bus-state entries empty when it asks none (the
bus module reads an empty one as "this module asks nothing now", design 25 § 12.2). A tool the indexer does not serve
yet answers "not known", and nothing retires on it. It is marked retiring only once no node-engine reports applied a
build that reads it, as the rename window is (§ above). mesh-config then emits `retiring` (family `config.events`,
kept seven days) and asks the operator once, through the operator channel, whether to remove it, naming any node that
still read it until lately; it is removed only on the operator's Approve. **mesh-config holds no secret**, so the
retiring flow and its one ask stay mesh-config's, and on the Approve it removes a value itself and retires a secret
through the seat `mesh-vault`'s new optional verb `retire` (one module's own secret, by node, module and name), carrying
the operator's warrant, which the vault checks (ADR 0259 §6) before it removes anything. The vault's caller list for
`retire`, a trusted config value in ADR 0363's form, names the holder of the seat `mesh-config` alone, checked against
the caller the tool runner proved; the warrant is bound to the node, module and name it was asked for, so it retires
nothing else. mesh-config then emits `retired` on `config.events` (module, name, node, the ask), and the vault emits
`own-retired` (node, module, name, the ask's id, the caller it proved) beside its `rotated` and `own-replaced`, on the
module events stream (ADR 0363) under the vault's own subjects, kept as that stream keeps every module's events. An ask unanswered for seven
days is asked once more, and then written as a finding, cleared by an answer, a cancel, or the value read again; a
sleeping node holding a retirement open for seven days is a finding naming it. `config_retiring` lists what is
retiring, since when, and its ask, and asks it again or cancels it.
**Where mesh-config keeps it.** Its new state (`renames`, `retiring`, `findings`) and its stream `config.events` are
declared in its old `state` and `streams` sections in this milestone, as the builder's and the checker's new state is
(design 51 *The move*), so that neither mesh-config nor the deciding parts that ask it wait on the bus module's
process, which is delivered in the same milestone. They move to bus state when each module next moves (ADR 0595 §11).
`config-changed` stays on the shared events stream, unchanged, so its consumers do not move.
**How the rename and the retiring are checked**, as cases of mesh-config's first whole-module integration test (C20,
C24 below), of the checker's and of the vault's existing whole-module tests: the checker's ask of `renames` meeting a
holder that does not serve it refuses `renamed-from` naming the verb, one meeting an open window refuses dropping
`renamed-from` naming the nodes, and a check that cannot ask fails saying so; `retire` with the warrant removes the
secret and emits `retired` and `own-retired`, without it, with a warrant for another name, or from a caller not on the list it is
refused; a waiter on the vault's `rotated` or `own-replaced` does not wake on `own-retired`; a node that was away,
returning with a build that reads the new name, closes the rename window on its `manifest-reads-changed` event when
it applied that build before the event (ADR 0618 §4), and on the next `delivered` when it applies it after; `config_rename_close` drops a node from a window and records the hand act; a window held seven days and a
retirement held seven days each write their finding naming the node, cleared when it closes.
### The mesh delivery cases this design carries
Research 081 listed the mesh delivery cases; these are the ones it handed to a design, each with its acceptance test,
each a case of its module's one whole-module integration test unless it says live.
| case | what holds | acceptance test |
|---|---|---|
| C6, a package series raised | users on `0.1` keep the newest `0.1`; a reviewed change moves a user | builder: a `0.2.0` publish moves no `0.1` user, its check takes the newest `0.1` too, a reviewed change naming `0.2` moves it, a user naming no series builds against the newest `0.1` |
| C14, a provision's form gains a field | the provider serves it in a new version; the consumer reads it once rebuilt; an older consumer ignores it | mesh-sdk: a consumer built against the older form reads a provision with the new field |
| C18, a new required config value with no default | the check names the nodes that lack it; the want waits; the operator is asked once | live: a module gaining such a value waits on each node lacking it, the operator is asked once naming the nodes, and once given the module is applied |
| C20, a config value renamed | the value moves, keeps its trust, both names read until every node runs the new build | mesh-config: a renamed value is present under the new name with no operator input and its trust; the old name answers until the node-engines report the new build; dropping `renamed-from` early, or changing trust, is refused; a refused move writes its finding |
| C22, a secret only a person can give | the want waits; the operator is asked once what to give and where | live: a module requiring such a secret waits, one ask names it and where, and once given the module starts |
| C24, a config value or secret no longer read | it is retiring, the operator is asked, removed only on the Approve | mesh-config: with one node-engine still reporting a build that reads the value it is not marked retiring and no ask is sent; once that node reports the new build it is marked retiring, `retiring` is emitted and one ask reaches the operator; an ask unanswered seven days is asked once more, then written as a finding; the value stays until an Approve removes it |
## 9. Provisioning over the bus
Provisioning rides the bus, and the provider stops having an address.
@@ -482,7 +656,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.
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
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.
**Something that is not on the bus: the address stays, exactly as before.** A module that requires
@@ -580,11 +754,11 @@ billing existing under that name.
## 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.
- **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 —
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
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
@@ -602,6 +776,45 @@ billing existing under that name.
- **A module reaches only the state it declared.** The composer's test: an owner's runtime may write
its buckets, a reader's may only read, and nothing else is granted; the runtime's test over a real
bus: a name not issued and a reader's write are refused with the reason.
- **Bus state is asked for once, in one form, and read by its owner alone** (ADR 0595 §8, ADR 0596). The manifest validator's
unit tests: a `reads` in the new form, a local name used twice across storage types or in an old section and the
new form, an unknown template and an option outside its bound are each refused, naming the field. Live: a moved
module receives its binding after `created`; another module's account reading its entries is refused; a moved
reader hears its owner's declared event and asks its owner's tool. The rest is [design 25](25-the-bus-on-nats.md)
§ 12.7.
- **State is current at once.** The runtime's test over a real bus: a watch hands the current values
without deletions, then an end-of-current marker, then changes. Live: a machine assigned after a put
reads it at start.
## Reviewed by architectural domain
The amendment of 2026-11-03 to ADR 0595, ADR 0596 and ADR 0597 (designs 25 § 12, 32 § 4 and 55 together) was
reviewed by the four reviewers by architectural domain side by side (playbook 01), over three rounds, and the lead
ruled on every finding. What was found, and what answers it:
| Domain | Found | Answered by |
|---|---|---|
| security | the author could be words the module chose; a person's dangerous tools had no role, mark or path to the process; the caller travelled as a plain field | design 25 § 12.3 (marks, the process's own tool subject, the identity seat's signed token), § 12.4 (the tool runner stamps the caller it proved; the move's authors marked imported) |
| seams | the bus module asked for its own events in the new form, which it would have to create itself (ADR 0595 §10); a person's tool had no join to the process; a verdict was written twice with no order | design 25 § 12.3 (its events on its old `streams` section; the process's own tool subject); design 55 § 2.1 (the `review` operation first, `reviews` caught up from it) |
| running qualities | overdue creation was undefined and raised by nobody; the database's rollback write lived in memory; a forward delivery after a rollback could miss acceptances and verdicts; no size estimate of the heads | design 25 § 12.2 (five minutes, the watchman, durable consumers, `check` at start); design 55 § 8.1 step 6 (`database.sequence`, the check at every start before the drop), § 2.1 (the heads' estimate) |
| health | the bus module's health would have fired on a quiet mesh; conditions had no clearing; `database-behind` had no clearing | design 25 § 12.2 and § 12.3 (each condition with what clears it); design 55 § 8.1 step 6 |
| events and logs | an event lost when the module stopped after `apply`; the bus module's events had no reader | design 55 § 1 (each event sent with its operation's sequence as id, the state `sent`); design 25 § 12.3 (`history` of one entry) |
| testing | the bus module had no test that starts it whole; live tests would have filled the live bus; the budget was not counted | design 25 § 12.7 (the nats module's integration test, live drift only by a manifest change, the budget paragraph); design 55 § 10 |
| upgrade | the build order lacked the grants part's binding and two undo steps | design 25 § 12.6 |
| tools | the ledger's repairs had no tool for the next owner; the bus module's events had no read tool | design 25 § 12.5 (served for every ledger owner), § 12.3 |
**Left to the operator**, because each changes an accepted record: a margin between raising and clearing the
condition `full` (ADR 0595 §6 raises and clears it at 75 percent, so an entry sitting there flaps), and an event of
its own for `drop` and `recreate`. **A correction of fact** for ADR 0595 §4: the bus module deletes through `recreate`
as well as `drop`. Each is noted on issue 1966.
The amendment of 2026-11-03 for research 081 (ADR 0606, ADR 0609; the package, rename, retiring and delivery-case
sections of § 8) was reviewed with designs 51 and 26 by the four reviewers by architectural domain; the findings and
what answers them are listed in design 51's *Reviewed by architectural domain*. For this design: a rename keeps a
value's trust (security), `packages` is a section of its own, not `uses`, and mesh-config hears the status view's
`delivered` events (seams), a value retires only once no node-engine runs a build reading it (security), and `renamed`
and `retiring` have a family and an ask that is asked again (events).
The milestone's amendment (ADR 0612, ADR 0613: the seat verb `renames` the checker asks, the vault's `retire`,
`manifest-accepted` for every manifest, mesh-config's new state in its old sections) awaits its own review by
architectural domain, which rewrites this section.
@@ -135,7 +135,7 @@ by side until nothing is bound to the old one.
*Amended 2026-10-07 (ADR 0246,
[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
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
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:
@@ -163,7 +163,7 @@ still a new version.
of the subject table.
- **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
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
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
@@ -0,0 +1,387 @@
---
layer: to-be
design: 34
milestone: every-event
updated: 2026-11-11
code: [mesh-sdk, mesh-delivery, mesh-tools, mesh-catalog modules/claude-code]
decisions:
- "ADR 0670"
- "ADR 0671"
- "ADR 0286"
- "ADR 0367"
- "ADR 0043"
- "ADR 0259"
- "ADR 0332"
- "ADR 0363"
- "ADR 0376"
- "ADR 0489"
- "ADR 0571"
- "ADR 0578"
- "ADR 0614"
---
# Design 34, milestone every-event — the tool runner hears every event
**What it builds.** A session's `mesh_wait` waits on any event of any seat or module, by its name, with no list to
keep: the tool runner's bus account is granted the event subjects of the whole mesh, every module's own stream
included (ADR 0671 §1), and the list of families in the tool runner's manifest, the manifest validator's rule about it
and the grants part's computation of subjects from it go (ADR 0670). The whole shape is in the design's main file, [§3b](../34-the-console.md#3b-waiting-on-the-bus); this
part holds every line the milestone builds, cited as "design 34 every-event §n".
**Where.** Four repositories and hq, one delivery group: mesh-sdk (the shared event subjects; the field, type and
membership entry that go), mesh-delivery (the grants part, seat `mesh-grants`; the manifest validator in the indexer),
mesh-tools (the tool runner, module `node-tools`) and mesh-catalog (the claude-code module's Stop hook, §20), with hq
for the playbook and agent lines (§24). Only these reference the list today: no other manifest carries the key
`waits`. The milestone is built on each repository's main as it stands. Design 47's milestone G edits the grants
part's account composition and the tool runner's manifest and manifest test too; its lead rebases it onto this
milestone's heads when G's develop restarts.
**The approval mark on events.** *A marked event waits for the operator's warrant* (§32 to §37) writes the operator's
answers of 2026-11-11: a wait respects the approval mark, and the gate that asks and checks it (design 46's milestone
7, [`7-approval-mark.md`](../46-milestones/7-approval-mark.md)) is built in design 47's milestone G and delivered in one
group with G and this milestone ("Build the gate in G"). So §25's grant of a restricted stream's subjects and the gate
that holds its mark reach the machines together.
## The event subjects, in mesh-sdk
1. **One place names the mesh's event subjects.** mesh-sdk's package `buscompose` gains a file `events.go` with three
exports, used by the grants part (§6) and the tool runner (§13), so neither spells the event namespace itself:
- `EveryEvent`, the subjects the tool runner's account hears: `mesh.seat.*.event.>` and `mesh.mod.*.event.>`, in
that order (ADR 0670 §1);
- `EventSubjects(name)`, the subjects one event name lands on. A name is `<emitter>.<event>`, optionally followed
by further tokens (`operator-channel.decided.claude-code`); each token matches `[A-Za-z0-9_-]+`. It answers four
subjects: `mesh.seat.<emitter>.event.<rest>`, the same followed by `.>`, `mesh.mod.<emitter>.event.<rest>`, and
the same followed by `.>`, where `<rest>` is every token after the first; given a namespace (`seat` or `module`,
§13) it answers only that namespace's two. It refuses, with an error naming the rule, a name of fewer than two
tokens, an empty token, or a token outside that form (so `*` and `>` are refused);
- `EventName(subject)`, the inverse for a heard subject: for `mesh.seat.<emitter>.event.<rest>` or
`mesh.mod.<emitter>.event.<rest>` it answers the name `<emitter>.<rest>` and its namespace; any other subject is
answered as not an event.
Why four subjects: a name does not say whether its emitter is a seat or a module, and both namespaces are heard
(§6); and a seat's event that names its asker or kind after the event (ADR 0259 §3), or a module's that names its
module and branch, is heard by the event's name alone, the `filter` narrowing it by body. How it is checked: one
table-driven unit test of the three, in `events_test.go`, one of whose rows holds that every subject
`EventSubjects` answers for its cases is covered by a subject of `EveryEvent`.
2. **The manifest loses `waits`.** `catalogue.Manifest.Waits` and its comment go from `go/catalogue/manifest.go`;
the comment on `ToolRunner` loses "and the only module that may declare `waits`". `go/catalogue/waits_test.go` is
deleted, and one row is added to the nearest table over raw manifests, `go/catalogue/invokes_test.go`'s
`TestAManifestMaySayWhatItInvokes` (Q-T): a manifest
`{"module":"node-tools","version":"1","tool-runner":true,"waits":["a.b"]}` decodes with `UnknownField()` naming
`waits` (ADR 0670 §2). The decoder itself is unchanged: a stored manifest that still carries `waits` is read
without it (ADR 0578 §3).
3. **The baseline of manifest reads stays as generated.** `go/catalogue/manifest_reads_baseline.txt` keeps
`word:waits`: it is the contract of the engines built before any stated what it reads (ADR 0614 §4), which did read
`waits`. In `own_manifest_test.go` the list of baseline entries this contract no longer reads (today the two
renamed seats) gains `word:waits`, its comment naming ADR 0670 §2 as the record that retired the word.
4. **The wait types go.** `go/buscompose/waits.go` (the type `Wait`) is deleted and `Users.Waits` goes from
`go/buscompose/users.go`; `Membership.Waits` and the type `WaitIssued` go from `go/membership/membership.go`
(ADR 0670 §2). Nothing replaces them: no membership names what its account hears.
## The grant, in the grants part (seat `mesh-grants`)
5. **What the grants part computed from the list goes.** `internal/buscompose/waits.go` is deleted whole (`Wait`,
`WaitSubject`, `waitGrants`, `waitsIssuedFor`); `Declared.Waits` goes from `internal/buscompose/users.go`, and with
it the term `len(d.Waits) > 0` of `runnerServes`; `m.Waits = waitsIssuedFor(d)` goes from `membership.go`, and the
alias `WaitIssued` from `sdk_membership.go`; `waitsOf` and the field `Waits:` of `declaredFor` go from
`internal/grants/records.go` (ADR 0670 §2). `safeSubject` in `nats.go` stays: the bus-state grants use it.
6. **The tool runner's runtime hears every event subject.** In `internal/buscompose/nats.go`, where a runtime's
permissions are composed from the modules it carries, the call of `waitGrants` is replaced by: when one of the
carried modules says `tool-runner` (`Declared.ToolRunner`), `buscompose.EveryEvent` (§1) is appended to the
runtime account's subscribe list. Nothing else is added for it: no publish, no durable consumer, no queue, no other
subject; a runtime carrying no tool runner gets neither subject (ADR 0670 §1, §3; ADR 0043 §2, §3, §5, §8 as ADR
0670 §3 now reads them). The grants part writes them into the bus's user list as every grant (ADR 0571 §9); they
are read with `mesh-grants.grants_user_list`, and composed at once with `mesh-grants.grants_compose_now`.
7. **The tool runner's membership names no events.** The grants part's first composition after its delivery rewrites
every tool runner's membership in its state `memberships` (key `<node>.node-tools`, ADR 0571 §10) without the key
`waits`, since the field is gone (§4); every other part of it is unchanged. It is read with the grants part's
reading of memberships and rewritten at once with `mesh-grants.grants_compose_now`.
8. **A composition test holds the grant.** In `internal/buscompose/buscompose_test.go`, one table-driven test composes
a runtime carrying the tool runner and one other module, and a runtime carrying the other module alone, and holds:
the first's subscribe list contains `mesh.seat.*.event.>` and `mesh.mod.*.event.>`; no subscribe subject of either
is wider than those (none of `>`, `mesh.>`, `mesh.*.>`, `mesh.seat.>`, `mesh.mod.>`, nor any `$JS.` subject beyond
what the carried modules' own consumes give); the first's publish list holds no subject of another module's or
seat's events; the first derives no durable consumer beyond its carried modules' `consumes`; and the second has
neither subject (ADR 0670 §1, §3).
9. **The real bus holds it too.** The grants part's existing real-bus test (`cmd/mesh-grants/composed_integration_test.go`)
gains cases, no new test: the tool runner's account, as composed, subscribes
`mesh.mod.mesh-delivery-view.event.delivered` and receives an event published there; it is refused a publish on
that subject; it is refused a subscription on `mesh.seat.channel.accept.show.<kind>` and on
`mesh.seat.intake.proof.code.<kind>` (an accept and a proof are never events, ADR 0670 §1); and another module's
account is refused a subscription on an event it does not consume (ADR 0670 §1, §3).
## The manifest validator, in the indexer
10. **The rule about the list goes.** `waitProblems` and its call in `judge/rules.go` are deleted, and with them the
regular expression `waitToken` where nothing else uses it; the table rows that judged `waits`
(`judge/rules_test.go`, the `waits` cases) are deleted, and one row is added: a manifest that carries `waits` is
refused by the manifest validator as the checker runs it (`Shelf`), naming the unknown field `waits`, and set aside
to wait as the indexer runs it (`ShelfWaiting`), as any field it does not know (ADR 0578 §1); the tool runner's
manifest without it is accepted (ADR 0670 §2). Only revisions taken in after the indexer's delivery are judged so:
a manifest the validator accepted before keeps its verdict (ADR 0578 §1).
## The tool runner (module `node-tools`)
11. **Its manifest's changes, every one.** `node-tools/module.json` drops the key `waits` and its five entries (ADR
0670 §2); gains `operator-channel` in `uses`, for the asks of marked waits (§35; design 46 7-approval-mark §7); gains
in its old `state` section the per-machine state `approvals` of design 46 7-approval-mark §13 (the tool runner is a
foundation part, ADR 0619 §2, §3); and gains the stream `node-tools-findings` and
the `consumes` of `mesh-conditions.findings-asked` that design 47 G §387 writes. Its `version` is `0.1`, as design 47 G §368
and §624 have every manifest's. Nothing else in it changes.
12. **The publication time is kept.** `bus.Heard` gains `Published`, read from the event's header `x-time`, which
mesh-sdk's emit sets (`go/process/emit.go`) and the status view's own emit too; `HearFirst` reads it from the
message's headers, and leaves it empty for an event that carries none.
13. **`mesh_wait` takes any event name.** `console.Surface.waitAnswer` (`internal/console/wait.go`) no longer reads the
membership. Each name of `subjects` becomes its subjects through `buscompose.EventSubjects` (§1); a refused name is
answered as a failure quoting that error. An optional argument `from`, `seat` or `module`, narrows every name to
that namespace, for an emitter whose name is both a seat's and a module's. A call naming more than 16 names is
refused, saying the bound (each name is four subscriptions for the call; 16 names is 64, far above what a watcher
names). The same subject named twice is subscribed once. The map from subject to name goes: the heard event is
named by `buscompose.EventName` of the subject it arrived on. The timeout, the filter and the timed-out answer are
unchanged (ADR 0367 §1). The refusal of names outside a list, and its words naming the manifest, go (ADR 0670 §2).
14. **A misspelt name is refused, not waited on for ever.** Before it subscribes, `waitAnswer` refuses a name whose
emitter is neither a seat nor a module of the catalog half the tool runner holds from G (its first view by
`mesh-catalog.revision`, followed on `catalog-revision`; design 47 G §196), naming that source and its revision.
A name whose emitter is known but whose event no seat definition and no module's entry in the catalog half
declares (its `emits` and its streams' events, design 46 7-approval-mark §5) is waited on,
and its timed-out answer says that no declaration names that event. While the catalog half is not yet read (the
first views' state, `mesh_first_views`), names are not checked, and the answer says `unchecked` with why; a
marked wait is then held as §34 says.
15. **The answer names the heard event.** A heard event is answered as today, `heard` with `subject`, `body` and
`at`, where `subject` is now the heard event's whole name (`mesh-delivery-view.delivered`,
`operator-channel.decided.claude-code`), with two new fields: `from`, `seat` or `module`, the namespace it
arrived on, and `published`, the event's own time (§12), absent when it carries none. The `read` line stays (an
event only wakes you, ADR 0367 §4); for a name beginning `operator-channel.decided` it says instead that the
operator's answer is read only with `claude_code_answer`, never from the event's body (ADR 0670 §5; ADR 0332 §5,
§6).
16. **A refused subscription is said, and a wait is never a hot loop.** `HearFirst` subscribes every subject of the
call; a subscription the bus refuses is noted, and the others go on waiting. When every subscription of the call is
refused, it waits out the call's timeout (or the caller's going) and then answers the refusal, so a watcher that
loops on it asks once a timeout, never at once again. Its refusal words become: "the bus refused this account a
subscription on <subjects>: the grants part grants this machine's tool runner every event subject (novox/hq ADR
0670 §1), so its grant is not composed yet; read the state from its owner meanwhile"; no words name a wait grant or
ADR 0367 §2. A heard event's answer and a timed-out answer list under `refused` any subject the bus refused while
others stood.
17. **The tool's description says what it hears.** `waitTool`'s description of `subjects` says: one or more event
names, `<seat or module>.<event>`, such as `mesh-delivery-view.delivered`; an event whose subject goes on after
the name (an asker, a kind, a module) is heard by the name too; the filter narrows by the body's fields; `from`
narrows to a seat's or a module's. Its own description keeps "an event only wakes you" and adds that an operator's
answer is read only with `claude_code_answer`. The words "only those the mesh granted this machine's tool runner"
go.
18. **The hearing serves `mesh_wait` alone.** `HearFirst` has one caller, `waitAnswer`. The modules the runtime carries
share its account, so the bus no longer bounds which events their code could hear; the runtime still gives a
carried module's code only what its membership names, and this is accepted under ADR 0670 §1, every session
already hearing every event. How it is checked: the code review of every change to the tool runner reads
`HearFirst`'s callers.
19. **What goes from the tool runner's code, and its tests.** The type `bus.WaitIssued` (`internal/bus/wait.go`) and
`Membership.Waits` (`internal/bus/bus.go`) go. In `console/manifest_waits_test.go` the test of the five families
becomes a test that the manifest has no key `waits`, its comment citing ADR 0670 §2; its tests of what the tool
runner consumes and of the runner's own secrets stay. In `console/wait_test.go`: `waitingRuntime` issues no waits,
and the answering test waits on `mesh-delivery-view.delivered` and on `operator-channel.decided` with no membership
naming either, its rows holding `subject`, `from`, `published` and `read` (a `decided.<asker>` event heard by
`operator-channel.decided`); `TestMeshWaitRefusesAnEventOutsideTheGrantedFamilies` becomes the table of refusals:
a name with `*`, with `>`, of one token, 17 names, an emitter no source knows (§14), and a bus-refused subscription
answered after the timeout with §16's words; `TestMeshWaitWithNoFamiliesIssuedWaitsOnNothing` is deleted. In
`bus/wait_test.go`, `TestTheBusRefusesAWaitOutsideTheGrant` stays as §16's refusal test, with a timeout of a
fraction of a second, its comment citing ADR 0670 §1. The manifest test also holds each change of §11: no `waits`,
`operator-channel` in `uses`, the state `approvals` in the old `state` section, per machine with its time to live,
and none in `requires`, and the finding stream. The
tool runner ends milestone G with no more tests than it starts with (design 47 G §419, §626): these tests replace
existing ones, and every test deleted counts.
## The claude-code module's Stop hook
20. **The hook no longer doubts the grant.** In mesh-catalog `modules/claude-code/cmd/claude-code/stop.go`,
`watcherAdvice` says for a wait with an event: "mesh_wait on <event> and reread <read> after every answer, or start
a watcher that does", with no condition on the tool runner's grant; its comment cites ADR 0670 §2 beside ADR 0489
§3. Its row in `stop_test.go` holds the new words.
## Events carry no secret
21. **The prepare lists every event.** The milestone file's prepare (playbook 07, step 0) gains a section *Events and
their fields*: every event each module declares (its `emits` and its streams' events) and each seat's definition
declares, read from the module catalog: the catalog revision of the change's branch (`mesh-catalog.revision`),
which carries the seat definitions too, and nothing else, no manifest and no bus read, so the events G and design
46's milestone 7 add are listed (the finding streams of design 47 G §383 among them), each a holder's event with
its subject form; and every event mesh-sdk publishes on a module's behalf (the announcement's `states` on
`mesh.mod.<module>.event.states`, found by the callers of its subject builders); each with the fields of its body
as the emitting code builds it, by file and line (ADR 0670 §4). *Why:* the operator, 2026-11-11: "only the indexer
uses the manifest to build the module catalog".
22. **The code review confirms none carries a secret, before the grant is delivered.** The milestone's code review
(playbook 07, step 2) reads that list again at the group's heads, and confirms that no event's body carries a
credential, a token that grants an act, code, or a message marked secret; an intake `choice`'s one-use token is not
such a token (ADR 0259 §3, §5). The list is the module catalog's (§21); the review reads no bus. An event found
carrying a secret holds the milestone's delivery and goes to the operator as a design question
(ADR 0670 §4).
23. **The stream events are read too.** §21's list also names every event kept in a module's own stream
(`mesh.stream.<module>.event.<event>`, ADR 0363 §2), the seat holders' events published there among them, and
whether its stream is restricted (its declaration carries a `grant`, ADR 0363 §4). The mail stream's events are
named in it as heard with their content: a mail's own text, a sign-in link or a code someone sent by mail among
it, is the operator's mail and not a secret in §22's sense, which is a credential, token or code the mesh itself
issues or keeps (ADR 0671 §3, which narrows ADR 0670 §4).
24. **From then on every change is asked the same.** In this milestone's hq pull request: playbook 07's review step
gains the question, for any change that adds an event or a field to one, "does it carry a secret?"; and the watcher
lines that describe the list (`.claude/agents/mesh-watcher.md`, playbook 07's *Following and reporting*: "the
families a watcher may wait on are those the mesh granted the tool runner") say instead that `mesh_wait` hears any
event by name (ADR 0670 §2, §4).
## Stream events
*By ADR 0671: the operator chose every module stream, mail too, on 2026-11-11.*
25. **The tool runner also hears every module stream, a restricted one included.** `buscompose.EveryEvent` (§1)
gains a third subject, `mesh.stream.*.event.>`, after the two of §1, and the grants part's composition (§6) appends
it with them. Nothing else is added for it: no durable consumer, no grant to acknowledge, no membership word, no
publish, and no subscribe deny of a restricted stream (ADR 0671 §1). A restricted stream's grant value (ADR 0363
§4) decides who consumes it, not who hears it, so changing or withdrawing that value changes nothing in the tool
runner's grant (ADR 0671 §2); a module that consumes a restricted stream is composed as today (ADR 0376 §2 as ADR
0671 §2 reads it). The bus does not hold the approval mark on a restricted stream; the tool runner's own gate does,
before it subscribes for a wait (§32 to §37; the operator, 2026-11-11), and that gate is delivered in the same
group as this grant (design 46 7-approval-mark §23). *Why:* ADR 0286 §8 has the bus hold a mark, and the bus
cannot grant a subscription for one approved wait alone, so the operator chose the tool runner's gate.
26. **A name hears its stream form.** `EventSubjects` (§1) answers six subjects, adding
`mesh.stream.<emitter>.event.<rest>` and the same followed by `.>`; `from` (§13) takes `stream` too; `EventName`
answers a stream subject with namespace `stream`. A seat's holder event kept in its holder's stream is named by its
holder module (`mesh-delivery-checker.verdict`). The bound of §13 stays 16 names (96 subscriptions).
27. **The tests hold it.** §8's test holds exactly the three subjects for the runtime carrying the tool runner, beside
a fixture module that declares a restricted stream, and that no durable consumer and no subject to acknowledge on
that stream are derived for the tool runner's runtime (ADR 0671 §1). §9's real-bus test gains cases: the tool
runner's account, as composed, receives an event published on a fixture module's stream, and one published on a
fixture restricted stream whose grant value has no entry for the tool runner, and still receives one after that
value is changed and the accounts are composed again; it is refused a durable consumer and an acknowledgement on
that restricted stream; and a module whose `consumes` names the restricted stream with no entry in its grant value
gets no durable consumer and no read grant on it (ADR 0671 §1, §2).
## Delivery
28. **In G's delivery group, no order named beyond the module graph's.** The members are pushed and checked in design
47 milestone G's group, with design 46's milestone 7, whose edges (design 46 7-approval-mark §23) order the gate
before the seat definition that marks `execute`; this milestone adds no order of its own between its members, and no
transitional field is kept (the operator, 2026-11-03: a change is designed as
it should be, its delivery is the delivery system's): a part built with the new mesh-sdk reads a stored manifest of
the tool runner that still carries `waits` without it (ADR 0578 §3), and a node-engine is given a want only for a
manifest whose words it reads (ADR 0614 §4).
29. **What `mesh_wait` answers between the deliveries.** On a node whose tool runner is new and whose grant is not yet
composed, the subjects the old grant names are still heard and the rest are listed under `refused` (§16); on a
node whose tool runner is older and whose membership the new grants part rewrote, the old runner answers at once
that it was issued no event families. Both are loud. A watcher reads its owner after every answer (ADR 0367 §4) and
ends at a call failed twice (its definition), so neither becomes a loop; a laptop asleep through the window gets
its new tool runner when it wakes, and its watchers read their owners meanwhile in the same way.
30. **Going back is going forward** (design 46 7-approval-mark §24 for the gate's part). A fault met after the delivery is fixed by a new change delivered the same way.
Reverting the group instead reverts every member, the indexer first, since the manifest validator delivered here
sets aside the old tool runner's manifest carrying `waits` (§10); until the revert reaches a node, `mesh_wait`
there answers as §29 says.
## Nothing built ahead of the group
31. **No tool is built ahead of the group.** The extension of the nats module's `nats_streams` this line held, for
§22's bus read, is not built: the secrets review reads the module catalog alone (§21, §22). *Why:* the operator,
2026-11-11: "only the indexer uses the manifest to build the module catalog". *How it is checked:* the milestone's
prepare names no member outside the group.
## A marked event waits for the operator's warrant
*By the operator's answers of 2026-11-11: "can it also respect any 'requires approval' config on the topics?", and
"Build the gate in G".* These lines use the gate design 46 7-approval-mark builds for a marked call (its §8 to §19),
delivered in the same group. *Why they depart from the records, said once for the section:* ADR 0286 §1 marks tools
and verbs, and the operator extended the mark to a module's events and streams; ADR 0671 §1 and §3 have every session
hear every module stream, mail included, with no exception, and the operator narrowed that to a wait the operator
approves, for a marked event or a restricted stream.
32. **A module marks an event, and a restricted stream is marked whole.** A module's manifest `needs-approval` (ADR
0286 §1) may also name one of its own events, by the name its `emits` or one of its `streams`' `events` gives it,
with `*` and `**` as there. Every stream a module declares with a `grant` (ADR 0363 §4; `ModuleStream.Restricted`)
is marked whole, every event it carries, with no entry in `needs-approval`; a stream without a `grant` is not, a
work-queue one included. A seat's definition marks no event. The indexer's manifest validator refuses, as the
checker runs it, a manifest whose `needs-approval` names an event that neither its `emits` nor its streams'
`events` carry, naming the event, as ADR 0286 §4 refuses a mark on a tool the module does not serve (design 46
7-approval-mark §4's table gains the row). A module that marks only events needs neither `operator-channel` in
`uses` nor `approval-mark/1`: the tool runner asks for the wait, not the module.
33. **The tool runner reads the marks from the catalog half on the bus.** The tool runner derives each module's marked
subjects from the catalog half it holds (§14; the catalog graph entry carries `needs-approval` and each stream's
`grant`, design 46 7-approval-mark §5): for a marked event `<event>` of module `<module>`,
`mesh.mod.<module>.event.<event>` and `mesh.stream.<module>.event.<event>`, each event pattern as the bus spells it
(`WirePattern`); for a stream with a `grant`, every subject of `ModuleStream.Subjects()`; and each of these also
followed by `.>`, since an event may name its node or asker after its name (design 47 G §383) and
`ModuleStream.Subjects()` answers only the patterns the stream stores. It derives them again on every revision it
hears, and no module's name is written in its code; it is design 46 7-approval-mark §5's derivation, extended to
events, following the running version as that line says. A mark added while a wait waits applies from the next
call, which that call's answer says. *Why:*
ADR 0286 §5 had the mark issued in a membership; no controller issues one now, a membership names no event (ADR
0670 §2), and the catalog half, which the indexer alone builds from the manifests, keeps the mark out of any bundle
as §5 required; from G the catalog half is where the tool runner reads which modules exist (design 47 G §196), and
no part but the indexer reads a manifest (the operator, 2026-11-11).
34. **Which wait is marked.** A wait is marked when any subject it subscribes (§13, §26, after `from`) overlaps a
marked subject of §33, that is, some subject matches both, a wildcard on either side. mesh-sdk's `events.go` (§1)
gains `Overlaps(a, b)` for it. Before the catalog half is read (`mesh_first_views`), a wait with any subject
outside `mesh.seat.` waits, within its own timeout, for the first view and is then judged; nothing is asked while
the marks are unread, and a wait still unjudged when its timeout ends is refused then, saying the marks are not
yet read, since a mark that cannot be read runs nothing (ADR 0286 §40; design 46 7-approval-mark §19). A wait
`from: seat` is never marked.
35. **A marked wait runs only on the operator's warrant for that one wait.** The call is an act as design 46
7-approval-mark §8 makes one: the address `mesh_wait`, the node, the module `node-tools` and the tool `mesh_wait`,
its build (the tool runner's own fingerprint, read from its `MESH_FINGERPRINT` at its start, design 47 G §205 and
§625; while it is unknown, a marked wait is refused, saying so), and every
argument given (`subjects`, `from`, `filter`, `timeout`), so the warrant binds the exact names, the filter and the
bound. The tool runner refuses, asks, judges and claims it as that part's §9, §10, §11 and §13 to §17 do for a
marked tool, asking on the seat `operator-channel` under its own module's name, `node-tools`, whose manifest gains
`operator-channel` in `uses` for it (that part's §7). The ask's headline is "Wait on <names> on <node>?"
(shortened as that part's §10 says), and its explanation names every event name one per line, the filter, and the
bound as the timeout in minutes, then the act's fingerprint. On a warrant it accepts, it subscribes, and the wait's
timeout runs from then. One warrant is one wait (that part's §15): a watcher looping on a marked event is approved
call by call. Its answer carries the line naming the approval and the warrant is recorded through
`mesh-hand-acts.warranted` (that part's §16), and `mesh_approvals` (that part's §18) lists the waits asked beside
the calls. *Why:* the operator, 2026-11-11: a marked wait runs only on the operator's warrant for that one wait,
shown with the exact names it waits on and its bound.
36. **A refused marked wait is said, and never a hot loop.** A marked wait with no warrant the tool runner accepts
subscribes to nothing. Its first call publishes the ask at once; every such call answers, as an error, design 46
7-approval-mark §12's or §17's words with "this wait" for "the call", when its timeout ends or its caller goes, as
§16 answers a refused subscription, or as soon as the router's record of its ask says it was chosen: the wait hears
the warrant event `operator-channel.decided.node-tools` for its ask's id and then reads the record, an event only
waking it, and answers at once, so the same call made again runs within the warrant's 30 minutes. *Why:* ADR 0286
§14 answers within 5 seconds, which a watcher's loop would turn into a call every few seconds for the ask's hour;
ending at the choice keeps a long wait from spending the warrant's window. An unmarked wait is heard as §13 to §16
say, with no ask. Every machine's marked waits ask under the one asker `node-tools`, so they share its open asks
and the router's holding back of the oldest beyond three (ADR 0666 §12, as G builds it), and the wait reads the
router's record through `Client.Where` under `node-tools`.
37. **The tests hold it.** Unit, as the budget counts them (`meshtest` runs its bus in the test's process): §19's table
of refusals in `console/wait_test.go` gains rows, no test function: a wait on a fixture module's marked event, and
one on a fixture stream with a `grant`, each with no warrant, subscribes nothing, asks once and answers design 46
7-approval-mark §12's words after its timeout; a wait with the most timeout a wait takes (ten minutes, ADR 0367
§1), its ask chosen at its second minute, answers at the choice, and the same call made then runs; the same wait
with a warrant bound to its exact names, filter and timeout subscribes and answers the event published after it,
so the restricted stream's event is heard only after the warrant; with a warrant for other names, another filter
or another timeout it is refused; the same names `from: seat` ask nothing; a wait named `<module>.<event>.<node>`
under a marked `<module>.<event>` is marked; and before the catalog half is read, a marked wait asks nothing, is
judged when the first view comes within its timeout, and is refused at its timeout when it does not. A marked wait while
the tool runner's own fingerprint is unknown is refused, saying so. The warrant is
written on the test bus as the router's record. `Overlaps` gains rows in §1's table (`mesh.stream.mail.event.mail.>` overlaps
`mesh.stream.mail.event.mail.*.message`; `mesh.mod.mail.event.x` does not overlap `mesh.stream.mail.event.x`), and
§32's refusal a row in the indexer's `judge/rules_test.go`.
## Acceptance
- **A watcher wakes on an event no manifest names** (the operator's acceptance; live, read by tool call): at the first
delivery after every node holds the new tool runner, a watcher's `mesh_wait` on `mesh-delivery-view.delivered`
answers the status view's `delivered`, its `at` less than a second after its `published`, with no rereading on a
timer; the tool runner (`node-tools`) is delivered to every node, and the grants part and the indexer to theirs,
read with `mesh-delivery.show` and each node's `node-engine.composition` (§6, §13, §15).
- **The grant is exactly what the record names** (unit, and integration on a real bus): the composition test of §8
and the real-bus cases of §9 pass.
- **The list is gone** (unit): the manifest validator as the checker runs it refuses a manifest carrying `waits`, the
indexer sets it aside, and the tool runner's manifest is accepted without it (§2, §10, §11).
- **`mesh_wait` refuses loudly and never loops** (unit): §19's table of refusals passes.
- **No event carries a secret** (the code review): the milestone file's *Events and their fields* is complete and the
code review's confirmation at the group's heads is written in it before the grants part is delivered (§21, §22).
- **An answer is never taken from an event** (the code review, and unit): in the members nothing reads an operator's
answer from a `decided` event's body, and `mesh_wait`'s answer for one says to read it with `claude_code_answer`
(§15).
- **A stream event wakes a watcher** (live, read by tool call): a watcher's `mesh_wait` on
`mesh-delivery-checker.verdict` wakes on the checker's next verdict (§25, §26; ADR 0671 §1).
- **The grant does not follow a restricted stream's grant value** (integration on a real bus): §27's cases pass
(ADR 0671 §2).
- **A marked wait runs only on the operator's warrant** (unit): §37's rows pass.
- **A wait on mail, as the operator sees it** (live, read by tool call and on the phone): a session's `mesh_wait` on
`mesh-mail.mail`, the mail module's restricted stream, asks "Wait on mesh-mail.mail on <node>?" on the phone with
its bound, and hears nothing before; it ends refused when its timeout ends or as soon as the operator chooses; the
same call made after the Approve hears the next mail, and the next call asks again (§32 to §36).
+107 -13
View File
@@ -1,11 +1,25 @@
---
layer: to-be
status: designed
code: [mesh-catalog, mesh-tools, mesh-controller]
updated: 2026-10-03
status: in-progress
code: [mesh-tools, mesh-host, mesh-controller, mesh-sdk, mesh-delivery, mesh-catalog modules/claude-code]
updated: 2026-11-11
decisions:
- "ADR 0670"
- "ADR 0671"
- "ADR 0286"
- "ADR 0043"
- "ADR 0259"
- "ADR 0332"
- "ADR 0571"
- "ADR 0363"
- "ADR 0376"
- "ADR 0489"
- "ADR 0578"
- "ADR 0614"
- "ADR 0197"
- "ADR 0195"
- "ADR 0175"
- "ADR 0367"
- "ADR 0160"
- "ADR 0159"
- "ADR 0152"
@@ -26,7 +40,7 @@ it put it there (ADR 0152).
## 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
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`,
@@ -40,7 +54,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
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;
- `listens` on a port `from: machine` — the filter opens nothing for it, because loopback is not outside;
@@ -72,7 +86,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
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
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
@@ -104,7 +118,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
`<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
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.
**What exists is what announced itself** (ADR 0197):
@@ -117,6 +131,64 @@ listed once per machine does not hold on the live mesh MCP server — postgres a
optional, answered by whichever instance replies. The address replaces that statement rather than
repairing it.
## 3b. Waiting on the bus
*By ADR 0367, as ADR 0670 and ADR 0671 change it.* Beside the five discovery tools of §3a and `mesh_runtimes`, the mesh MCP
server offers a seventh, `mesh_wait`: a session names one or more events, an optional `filter` of body fields and a
`timeout` (four minutes when absent, ten at most), and is answered the first matching event or told it timed out
(ADR 0367 §1). A session calls it in a loop from a background command (ADR 0367 §3), and after every answer or time-out
reads the state once from the verb that owns it: an event only wakes it (ADR 0367 §4).
```mermaid
flowchart LR
E[a seat's, a module's or a module stream's event<br/>mesh.seat.&lt;seat&gt;.event.… / mesh.mod.&lt;module&gt;.event.… / mesh.stream.&lt;module&gt;.event.…] -->|heard for the call| TR[the tool runner's account]
G[seat mesh-grants] -->|grants the three event subjects| TR
M[the catalog half, seat mesh-catalog] -->|the marked events and restricted streams| TR
TR -->|a marked wait only on the operator's warrant| OC[seat operator-channel]
TR -->|mesh_wait answers it by name| S[a session]
S -->|then reads the state| O[the verb that owns it]
```
**The tool runner hears every event of the mesh, and no list says which** (ADR 0670 §1, §2). The grants part (seat
`mesh-grants`) grants the account of the runtime carrying the tool runner a plain subscription on the event subjects
of every seat and every module, and on nothing else of theirs: no accept, proof, call, work queue or bucket, and no
publish. `mesh_wait` subscribes when called and closes the subscription when it answers, so the account keeps no
durable consumer and no queue of its own (kept from ADR 0367 §2), and any event name is accepted: a new event is
heard from its first publication, with no change to a manifest. The tool runner's manifest names no events, the
manifest validator has no rule about them, and no membership lists them. A name whose emitter no seat and no module
bears is refused, so a misspelt wait does not run for ever.
**Who hears.** Whoever may call the mesh MCP server: any process on the machine, through its loopback endpoint (§2),
which is the session there. The tool runner's standing grant of every event is accepted under ADR 0670 §1: what a
session hears through it, it may hear.
**Module streams too, mail included** (ADR 0671 §1, §2). The tool runner also hears the events kept in a module's own
stream (ADR 0363 §2), the seat holders' events published there among them, a restricted stream such as mail's
included. A restricted stream's grant value decides who consumes it, never who hears it, so the tool runner's hearing
needs no `consumes` and no entry in that value, and keeps no consumer of it.
**A marked event is heard only on the operator's warrant** (the operator, 2026-11-11). The approval mark (ADR 0286) covers a module's events and streams as well as its tools and
verbs: a module may mark one of its events in `needs-approval`, and a restricted stream is marked whole. The tool
runner reads the marks from the catalog half on the bus (seat `mesh-catalog`), and a `mesh_wait` on a marked event, or on any subject
of a marked stream, runs only on the operator's warrant for that one wait, asked on the seat `operator-channel` with
the exact names it waits on and its bound, and checked before the tool runner subscribes; without one it is refused
and says so. The bus grants the subjects, and the tool runner's own gate holds the mark, as the operator chose; the
gate is design 46's milestone 7, built in design 47's milestone G and delivered in one group with G and milestone
every-event, so the subjects and the gate reach the machines together. This departs from ADR 0286 §1 and §8 (tools and
verbs only, held by the bus) and from ADR 0671 §1 and §3 (every module stream heard with no exception) by the
operator's answer. An unmarked event is heard with no ask.
**What that makes audible, and what still bounds it.** Every session now hears the operator channel's `decided` of
every asker and the intake bench's events. So no event of a seat or a module carries a secret: no credential, token or
code the mesh itself issues or keeps, and no message marked secret, which the code review asks of every change that
adds an event or a field (ADR 0670 §4 as ADR 0671 §3 narrows it). A mail's own content is the operator's mail and is
heard as it is, a sign-in link or a code someone sent by mail among it (ADR 0671 §3). And a heard event never answers anything: the operator's answer to an ask is read only
with `claude_code_answer`, from the router's record and held to the question's digest (ADR 0670 §5, ADR 0332 §5,
§6), and an asker acts only on a warrant from its own `decided` subject through its own consumer (ADR 0259 §6).
What is built for this, line by line, is milestone every-event's part,
[`34-milestones/every-event.md`](34-milestones/every-event.md).
## 4. Where it runs
On whichever machines an operator sits at, by assignment. It is not on the control node by default and
@@ -143,6 +215,14 @@ address gets a refused connection, which is the truthful answer.
| a call through the mesh MCP server's endpoint reaches a module over the bus and the answer is the module's own, unshaped | ADR 0035, a surface decides nothing |
| on the live mesh: the mesh MCP server assigned to a workstation answers `tools/list` on loopback and a call to the forge returns repositories | the exit of work-order step 3 |
| the composed filter for a machine carrying the mesh MCP server opens no port for it | ADR 0144 |
| on a real bus, `mesh_wait` answers the first matching event within a second of its publication, and after every answer a session reads the owner's state | ADR 0367 §1, §4 |
| live: a watcher's `mesh_wait` on the status view's `delivered`, an event no manifest names, wakes on it with no rereading on a timer | ADR 0670 §2, milestone every-event |
| a composition test: the tool runner's runtime account is granted exactly `mesh.seat.*.event.>`, `mesh.mod.*.event.>` and `mesh.stream.*.event.>` beside what its carried modules declare, with no publish on another's events, no durable consumer for them and no wider subject; on a real bus it is refused a publish on an event subject, and another module's account a subscription on an event it does not consume | ADR 0670 §1, §3; ADR 0671 §1 |
| on a real bus, the tool runner's account hears a restricted stream's event with no entry in its grant value, still after the value changes, and is refused a durable consumer and an acknowledgement on it | ADR 0671 §1, §2 |
| the tool runner's gate refuses a marked wait with no warrant and accepts one with a warrant bound to its exact names, filter and timeout; on the test bus a restricted stream's event is heard only after the warrant | the operator, 2026-11-11; ADR 0286 |
| the manifest validator, as the checker runs it, refuses a manifest that carries `waits` as an unknown field, and accepts the tool runner's without it | ADR 0670 §2 |
| the milestone's prepare lists every event with its body's fields, and its code review confirms none carries a secret before the grant is delivered; every later change adding an event or a field is asked the same in its review | ADR 0670 §4 |
| nothing takes an operator's answer from a `decided` event's body; `claude_code_answer` is its one reader | ADR 0670 §5 |
## What shipped, 2026-09-30
@@ -157,17 +237,31 @@ 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):
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
matched it by URL.
## Milestones
| Milestone | What it builds | State |
|---|---|---|
| the mesh MCP server | §1 to §5: the module, its loopback endpoint, its discovery (ADR 0152) | delivered 2026-09-30 (above) |
| found by address | §3a: five announced tools and addresses (ADR 0195, ADR 0197) | delivered; [as-is 13](../00-as-is/13-the-console.md) |
| waiting on the bus | §3b's `mesh_wait` on a list of five families in the tool runner's manifest (ADR 0367) | delivered (issue 745); [as-is 13](../00-as-is/13-the-console.md) |
| every-event | §3b as ADR 0670 and ADR 0671 change it: the tool runner hears every event, module streams included, a marked one on the operator's warrant, and the list goes; one delivery group of mesh-sdk, mesh-delivery, mesh-tools and mesh-catalog, with hq | designed; its part is [`34-milestones/every-event.md`](34-milestones/every-event.md) |
## What this does not settle
- Narrowing a mesh MCP server's grant per assignment. ADR 0046 makes it a setting; nothing reads one yet.
- The mesh's own verbs on the bus. Design 33's third family; this document only says where they appear
once they exist.
- A person's identity behind the mesh MCP server. The mesh sees the mesh MCP server's account; design 15 keeps the
question open.
- **The mesh's own verbs on the bus.** Design 33's third family: these now answer directly, through
the `mesh-controller` seat, under ADR 0154 — settled, not open. [As-is 13](../00-as-is/13-the-console.md)
records it live.
- **Narrowing the tool runner's grant per assignment.** ADR 0046 makes it a setting; nothing reads
one yet. Owned by [design 58](58-agent-work-as-the-meshs-own.md) milestone 6 (issue 883): an
identity asked for each assignment is one of that milestone's parts.
- **A person's identity behind the mesh MCP server.** The mesh still sees only the tool runner's own
account, never the caller's; design 15 raised the question and leaves it open. Owned by the same
milestone (design 58 milestone 6, issue 883): its outcome is that every person, agent and module
becomes an identity with a role, and a call carries which one made it.
## References
@@ -2,8 +2,10 @@
layer: to-be
status: in-progress
code: [mesh-catalog modules/claude-code]
updated: 2026-10-13
updated: 2026-10-17
decisions:
- "ADR 0411"
- "ADR 0412"
- "ADR 0343"
- "ADR 0274"
- "ADR 0273"
@@ -130,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
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
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
words, none of the predecessor's.
@@ -155,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
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
> **Revised 2026-10-03, building it.** The vendor's managed-settings key for tool servers refuses any
@@ -219,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
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
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.
@@ -306,21 +318,27 @@ operator's word — and, for the operator's own home, only at the terminal or on
## 9. The account the agent runs as
*Added 2026-10-08 by ADR 0266.*
Everywhere above, "the operator's home" and "the operator account" mean **the account the agent runs as**.
That is the operator account, unless the node names an **agent account** of its own. The control node does.
*Added 2026-10-08 by ADR 0266; changed 2026-10-17 by ADR 0411 and ADR 0412.*
Everywhere above, "the operator's home" and "the operator account" mean **the account the agent runs as**:
the machine account of the identity the module runs for (ADR 0412). claude-code declares that it can run for
an agent identity, in its assignment or when a session is started: started for `nox` it runs with the agent's
home; started for the operator, it is the default run, with the operator's configuration. On a
machine where no agent is marked as the agent account, its assignment is the default run, as every module's is.
**The fact.** The operator states the agent account at the controller's terminal. No verb and no setting
writes it. The controller resolves the machine facts `agent-account`, `agent-home` and `agent-root`, and gives
the tool runner the account and its home. Where the node names none, the facts name the operator account and
`agent-root` is empty, so nothing changes there.
**The fact.** The machine accounts come from the identity seat (ADR 0411); the operator no longer states the
agent account at the controller's terminal. A machine's agent account is the machine account of the agent the
identity seat marks as the agent account there, on the operator's Approve: `nox` on the control node. There this module's
assignment is run for that agent, and the controller refuses one that is not. The controller resolves the
machine facts `agent-account`, `agent-home` and `agent-root` from it, and gives the tool runner the account and
its home. Where no agent is marked (every other machine today), the facts name the operator account and `agent-root`
is empty, so nothing changes there, though a login `nox` exists for the modules run for it.
**What the module declares.**
- A `user` named by `agent-account`, with `root` set from `agent-root`. Where the account is the agent's own,
the mesh creates it (home, own group, no password) and the node-engine judges on every look that it cannot
become root without a person, by the ways ADR 0266 §3 lists. It detects those ways and no others; it
prevents nothing.
- No login. The mesh makes the agent's machine account (home, own group, no password) from the identity, 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.
It detects those ways and no others; it prevents nothing. The `user` resource named by `agent-account`,
which this module declared until ADR 0411, goes.
- `~/.claude` under that account's home, owned by it.
- `claude-agent`, a command that starts the agent as that account through the operator's own sudo, or
directly when already run as it.
@@ -397,6 +415,7 @@ the helper of §9 where that is an agent account):
| `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 |
| `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 |
| `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
**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
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
@@ -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.
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
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.
3. **`zsh`**, the first environment module: seat, `user` shape, home files, `execute`. Proven on a
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 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 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
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.
*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,
is refused; a module the catalogue already holds in that shape — judged from the manifest 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 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
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.
**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
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
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"
([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,
`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`
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),
[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).
@@ -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;
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
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
question research research 020 opened
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.*
**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
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
@@ -1,9 +1,10 @@
---
layer: to-be
status: implemented
code: [mesh-catalog modules/claude-licence-manager]
updated: 2026-10-04
status: in-progress
code: [mesh-catalog modules/claude-licence-manager, mesh-vault]
updated: 2026-10-26
decisions:
- "ADR 0550"
- "ADR 0209"
- "ADR 0206"
- "ADR 0183"
@@ -18,13 +19,35 @@ decisions:
# 39 — The Anthropic licence manager
**One module knows every Anthropic licence the mesh has, keeps each alive, decides which consumer gets
which, and hands every node's agent its token over the bus.**
**One module knows every Anthropic licence the mesh has, keeps each alive, and decides which consumer gets
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
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),
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
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 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
@@ -77,7 +101,9 @@ Carried from the predecessor, where each rule was earned by an incident:
## 4. Handing a token to a node
*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
**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
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
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
@@ -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 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 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
@@ -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 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) |
| 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 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 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 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 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.
**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
@@ -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;
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
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.
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 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
([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.
## 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.
**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
@@ -2,7 +2,7 @@
layer: to-be
status: in-progress
code: [mesh-host, mesh-controller, mesh-catalog]
updated: 2026-10-16
updated: 2026-11-04
decisions:
- "ADR 0394"
- "ADR 0310"
@@ -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,
`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
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.
**Proof.** The controller's tests:
@@ -157,7 +157,7 @@ against WP2's controller before anything is published.
- each refusal, by name;
- 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
@@ -188,7 +188,7 @@ The catalogue check over the whole catalogue passes.
- **`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.
**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:
- 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.
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.
4. On one server, assign `node-env`, `zsh`, `zsh-autosuggestions` and `zsh-syntax-highlighting`, and
push. Then check:
@@ -262,7 +262,7 @@ group**: the lighting daemon's, the container runtime's, a serial port's. It dec
│
node-engine usermod --append ── recorded on openrazer.account: "the mesh added openrazer"
│
account judge database lists it? ── running manager holds it? ── no: relogin needed
account probe database lists it? ── running manager holds it? ── no: relogin needed
│ (module openrazer, kind account)
controller module.openrazer.<machine>.unhealthy, cleared on the first look after a new login
```
@@ -280,7 +280,7 @@ group**: the lighting daemon's, the container runtime's, a serial port's. It dec
when the resource is undeclared, is given back. It is not given back while another declared resource
asks for it. The account is taken out of that one group (`gpasswd --delete`), read back, and a failure
is said and is not fatal, as giving back a shell is (WP1).
- **The node-engine, looking.** An account judge sits beside liveness and the failed-unit reading
- **The node-engine, looking.** An account probe sits beside liveness and the failed-unit reading
(issue 315). On every look it reads, for each account a module puts in groups, the database's groups
and the groups of the account's running manager, from that process's status file. It states each as the
module's resource of kind `account`: healthy, or unhealthy with *not in the group* or *relogin needed*.
@@ -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
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.
**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.
- **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
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
@@ -2,7 +2,7 @@
layer: to-be
status: in-progress
code: [mesh-catalog, mesh-controller, mesh-host]
updated: 2026-10-14
updated: 2026-10-22
decisions:
- "ADR 0369"
- "ADR 0303"
@@ -26,6 +26,9 @@ decisions:
- "ADR 0164"
- "ADR 0165"
- "ADR 0166"
- "ADR 0507"
- "ADR 0508"
- "ADR 0510"
---
# 42. The machines' modules, in order
@@ -47,7 +50,7 @@ not before. Modules that need none go ahead now.
## 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.
2. Merged, which builds it.
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 |
| 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 |
| 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 |
| 6 | `avahi` | the daemon | on all four, owned by none |
| 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 |
| 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 |
| 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
(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 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:
- `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
@@ -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
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'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`.
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.
## Phase 3 — one machine model
@@ -183,6 +213,6 @@ and `memory-pressure` (research 027/03).
## 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
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
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
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.
@@ -2,7 +2,7 @@
layer: to-be
status: in-progress
code: [mesh-controller, mesh-host, mesh-tools, mesh-catalog, mesh-sdk, mesh-lab]
updated: 2026-10-14
updated: 2026-11-04
decisions:
- "ADR 0357"
- "ADR 0325"
@@ -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
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
`operator-channel` seat's holder and channels, the watcher, the providers' retirement, the catalogue's
merge gate), **mesh-sdk** (the TypeScript providers' loop and its retirement), **mesh-lab** (the
`operator-channel` seat's holder and channels, the watcher, the providers' retirement, the catalog's
composition check), **mesh-sdk** (the TypeScript providers' loop and its retirement), **mesh-lab** (the
replays and the induced-failure scenarios).
---
@@ -320,7 +320,7 @@ producer that wrote it.
| 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 |
| 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 |
@@ -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
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
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
from a documentation range. It is kept in the artifact store as `facts/latest`, where the build seat
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
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
@@ -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
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.
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.
- **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
@@ -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
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
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
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 |
|---|---|
| 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 |
**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
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
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
`merge-check.sh`.
@@ -931,8 +931,8 @@ and "the commit is the build at hand"), on branches, not yet merged:
| 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-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-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 the program it composes with 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 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-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` |
@@ -0,0 +1,421 @@
---
layer: to-be
design: 46
milestone: 7-approval-mark
updated: 2026-11-11
code: [mesh-sdk, mesh-contracts, mesh-delivery, mesh-tools, mesh-host, mesh-catalog modules/zsh, mesh-catalog modules/gitea, mesh-catalog modules/mesh-files]
decisions:
- "ADR 0286"
- "ADR 0259"
- "ADR 0627"
- "ADR 0656"
- "ADR 0666"
- "ADR 0668"
- "ADR 0595"
- "ADR 0597"
- "ADR 0310"
- "ADR 0394"
- "ADR 0619"
- "ADR 0268"
- "ADR 0564"
---
# Design 46, milestone 7 — a call that needs the operator's approval
**What it builds.** A tool or a seat verb marked in its manifest or its seat's definition runs only on the operator's
approval of that one call, with its exact arguments (ADR 0286). The first call asks the operator on the seat
`operator-channel` and runs nothing; the same call made again within 30 minutes of the operator's Approve runs once.
The tool runner (module `node-tools`) asks, judges and claims; it reads the marks from the catalog half on the bus; and
on a machine whose tool runner does not honour the mark the grants part withholds every marked subject. The shape is in
the design's main file,
[§10, *A call that needs the operator's approval*](../46-the-conversation-with-the-operator.md#a-call-that-needs-the-operators-approval);
this part holds every line the milestone builds, cited as "design 46 7-approval-mark §n".
**When.** It is built and delivered in design 47's milestone G, in G's delivery group, with design 34's milestone
every-event, whose marked waits (design 34 every-event §32 to §37) use this gate (the operator, 2026-11-11: "Build the
gate in G").
**Where.** mesh-sdk (the manifest field, the seat definition's field, the per-machine state's keys), mesh-contracts
(the login shell's seat definition), mesh-delivery (the indexer's manifest validator and catalog graph entry, the grants
part's deny), mesh-tools (the tool runner's gate, its state, its tools, and the mesh MCP server's `mesh_describe` in
it) and mesh-catalog (the zsh module, which holds the login shell, and the forge module, §21). Nothing is built in a controller: no controller issues memberships today, bus permissions are the grants part's
(seat `mesh-grants`), the hand-act log is the seat `mesh-hand-acts`, and the asker's logic is operator-channel's client
in mesh-contracts (ADR 0627 §1).
**What changes from ADR 0286 as written.** Each line says in its *Why* where something built since ADR 0286
(2026-10-10) changes how a clause is met: the mark is read from the catalog half and not from a membership (§5), the
digest is Go alone (§8), the claim is kept in the runner's bus state and not a file (§13), `warranted` is a kept call
and not a retry on a timer (§16), and a fourth open ask is offered as G makes it (§17). The controller's own verbs (ADR
0286 §43) are not built: no controller serves verbs.
**The bound of the gate, a known limit.** The gate holds for every call that reaches the tool runner's handler. It does
not hold for a process that reads the tool runner's bus credential (the file `broker` in its state directory,
`own-secrets`) and speaks to the bus itself: that account may subscribe a marked subject, a restricted stream's among
them, on a machine whose runner honours the mark. Read on the workstation on 2026-11-11 with `node-reads.stat`, the
file is mode 0600, owned by the operator's account, in a directory of mode 0755, and the prepare of 2026-11-11 read
the same mode and owner on all four nodes: every process running as the
operator's account, every module on the machine's shared account and every agent session among them, can read it and
hear a restricted stream without a warrant. This is the same known limit as design 47 G §385 (modules running as the
machine's account, issue 1966 note 193, whose fix carries its check), and this milestone does not change it: that a
restricted stream is heard only on a warrant holds against every caller once that fix runs modules and sessions on
accounts that cannot read the file, and until then against callers that go through the tool runner. The
prepare reads the file's mode and owner on every node with `<node>/node-files.stat` and writes them in the milestone
file; a node where the file is readable beyond the operator's account is a finding of the prepare. The forge module's
own credential (§21) lies in a directory of mode 0700, owned by the forge's account, which the reading account cannot
enter; `node-files.stat` answers such a file's mode from this milestone (§21a), and the prepare reads it then.
## The mark
1. **A module marks its tools and verbs in `needs-approval`.** mesh-sdk's `catalogue.Manifest` gains `NeedsApproval`,
the manifest key `needs-approval`: a list of strings, each a tool the module serves (`<tool>`) or a seat verb it
serves (`<seat>.<verb>`), and, under design 34 every-event §32, one of its events. Absent is no mark. *Why:* ADR
0286 §1, §49 (each marking is a manifest change, reviewed like any other). Only the contract's manifest reads
(`ManifestReads`) gain `word:needs-approval`; the baseline of the engines before any stated what it reads
(`manifest_reads_baseline.txt`) is unchanged, since none of them reads the word (Q-BL). *How it is checked:* unit, a
row in `go/catalogue/invokes_test.go`'s `TestAManifestMaySayWhatItInvokes`, the nearest table over raw manifests
(Q-T): a manifest naming a tool, a seat verb and an event in `needs-approval` decodes all three with `UnknownField()`
empty, and one without it has no mark.
2. **A seat's definition marks a verb for every holder.** mesh-sdk's `seatdef` reads a verb's `needs-approval`, a
boolean, in a seat definition (`seats/<seat>/seat.json` in mesh-contracts); the seat definitions the catalog half
carries (design 47 G §194) carry it on the verb. The definition format's own schema in mesh-contracts
(`format/seat-definition.schema.json`) gains a verb's `needs-approval`, a boolean (Q-SC). Every strict reader of a
seat definition reads the word from G's reader release (design 47 G §340), so no reader refuses a definition that
carries it (Q-RR). *Why:* ADR 0286 §2. *How it is checked:* unit, a fixture row in `go/seatdef/seatdef_test.go`'s
`TestADefinitionIsReadStrictlyNamingWhatIsWrong` (Q-T), and the schema validates the login shell's definition of §21.
3. **Marks add up.** A verb is marked when its seat's definition or its holder's `needs-approval` marks it; a holder
cannot unmark what its seat marks. *Why:* ADR 0286 §3. *How it is checked:* unit, a row of §5's table.
4. **The manifest validator refuses a mark it cannot honour.** The indexer's manifest validator (`judge/rules.go`)
refuses, as the checker runs it, and sets aside to wait, as the indexer runs it, a manifest that: marks a tool its
`tools` does not list, or a verb its `claims` do not serve (ADR 0286 §4); marks a tool or a verb, or holds a seat
verb its seat marks, without `operator-channel` in `uses` (§7); marks a tool or a verb while every artifact of kind
`bundle` it builds is run by one of its `process` resources (`artifact`), so its own program answers its tools and
no tool runner loads or gates them (a module such as the forge's, whose tools bundle no process runs, is not
refused); or names in `needs-approval` one name that is both one of its tools and one of its events. A tool mark
is checked against `tools` where the manifest lists its tools; where only its bundle names them, the tool runner
checks each marked tool against the tools the bundle registers at load: a bundle whose marks name a tool it does not
register is not loaded, so no tool of that module is served on that node at all, failing closed rather than serving
unmarked what the manifest meant to mark, and that is its
finding `<node>.<module>.mark-unserved` (said as §16's), cleared when a bundle that serves it, or a manifest without
the mark, is loaded. Each refusal names the entry and the rule. *Why:* a mark
nothing enforces would be a call that runs unasked while saying it is guarded; ADR 0286 §43 had the controller
enforce its own verbs, and no other own-process server has a gate, so such a mark is refused rather than ignored;
and an entry that names two things is read one way by one reader and another way by the next (the design's choices
under ADR 0286 §4, §40, the refusal kept by lead-47 on 2026-11-11). *How it is checked:* unit, one row per refusal
in `judge/rules_test.go`'s `TestRulesManifests` (Q-T), rows that accept the zsh and forge manifests of §21, and a row of §22's mark table for
`mark-unserved`.
## Where the marks are read, and where they are withheld
5. **From the catalog half on the bus, never from a bundle.** The indexer's catalog graph entry of a module (design 47
G §174) carries the validated manifest whole (design 47 G §443), its `needs-approval`, its `emits` and its streams
(their events as the manifest spells them, and whether each carries a `grant`) among it; mesh-sdk's `buscompose`
exports `WirePattern`, which a reader uses to turn a stream's events into the bus's subjects, the grants part's own
copy going (Q-WP). No part but the indexer reads a manifest: every other part reads the entry of the build concerned
from the catalog half (design 47 G §442; the operator, 2026-11-11: "only the indexer uses the manifest to build the
module catalog"; Q-MR), the grants part's `declaredFor` among them for every grant, not only the marks; the seat
definitions of the catalog half carry the verb marks (§2). The tool runner, which from G takes its first view of the
catalog half by `mesh-catalog.revision` and follows `catalog-revision` (design 47 G §196), derives from it, for each
module it serves, its marked tools and verbs, in a package of its own, `node-tools/internal/approval`, again on
every revision it hears. No module's name is written in its code. The marks follow the running version: a bundle's
served version is the fingerprint its composed environment carries (design 47 G §424), whose entry the runner reads
by `mesh-catalog.entry` (design 47 G §443) when it loads the bundle; it applies that entry's marks and, while the
newest revision's entry for that module differs, the union of both, so a mark is never lost before or after the
module moves, and a delivery that drops a mark while the old bundle still runs is still asked. When the served
entry cannot be read, every marked call on that runtime is refused (§19). A mark added while a call waits applies
from the next call, which the answer of §12 says. *Why:* ADR 0286 §5 had the controller issue the mark in a
membership so that a bundle could not drop it; no controller issues memberships now, and the judged catalog half is
the same guarantee: the bundle never carries the mark (ADR 0656 §6, one source for which modules exist). The union
keeps ADR 0286 §3's "marks add up" across a version change. The served version is read from what the bundle runs,
not from a placement, which can change before the bundle does (step 0b). *How it is checked:* unit, a table test in
`approval` derives the marks from fixture catalog halves and served entries, §3's row, a version change and the
union among them, and a served entry that cannot be read.
6. **A machine whose tool runner does not honour the mark serves no marked tool or verb.** The tool runner states
`approval-mark/1` among its call capabilities (`runtime.CallCapabilities`), which its announcement carries with
the tool runner's own fingerprint, and its node's node-engine takes both into the node's record; the assignment
half carries, for each node, its capability readings, each with the fingerprint of the runner that announced it
(design 47 G §194; Q-CR). The grants part, composing a runtime's account, reads that runtime's node's readings and
the fingerprint the node-engine's `running.<node>.node-tools` last said running (design 47 G §309): where the tool
runner does not state `approval-mark/1`, where the node has no readings, or where the readings' fingerprint differs
from the running one or is absent (an older reading, failing closed as §19; no time is compared), it composes a
subscribe deny of every marked subject of every module the runtime carries, the marks read from the catalog half
as §5 has them: `mesh.mod.<module>.tool.<tool>` and the same followed by `.*`; the caller-named subject mesh-sdk's
`process.Call` sends on first, `mesh.mod.<module>.call.<tool>.>`; for a marked verb of a seat held per node
`mesh.seat.<seat>.tool.<verb>`, the same followed by `.<node>`, and `mesh.seat.<seat>.call.<verb>.<node>.>`; and for
a marked verb of a seat held once for the mesh `mesh.seat.<seat>.tool.<verb>` and `mesh.seat.<seat>.call.<verb>.>`,
since its subjects name no node (Q-DL). The bus's deny outranks its allow of `mesh.mod.<module>.tool.>` for each.
Going back to an older tool runner on a node leaves the window until its readings update, in which the old deny or
none stands; that window is accepted. `Permissions` gains `SubscribeDeny`, written into the user list as the
server's subscribe deny, as `PublishDeny` is for publishing; the user list's `User` gains `subscribe_deny` beside
`publish_deny`, in its comparison and in the differences the grants part says (Q-SD). An added `subscribe_deny` is a
narrowing: the user-list guard lets it pass at once, on the first composition after the release, and counts it as
no right lost. The withheld tools and verbs are named in the grants part's read of that account
(`mesh-grants.grants_user_list`, a field `withheld` with each subject and "its tool runner does not state
approval-mark/1"), and in the node-engine's `composition` of the module, which reads them through a verb `withheld`
that the seat `mesh-grants`'s definition gains: class `read`, callers the node-engine, persons and the operator,
input `node`, answer each module's withheld subjects and why, with a fixture in the definition; the node-engine's
manifest gains `mesh-grants` in `uses`; when the verb does not answer, the composition says "withheld unknown: the
grants part does not answer"; never read from the grants part's state (Q-NE). *Why:* ADR 0286 §7, §8, §9: the bus
enforces the mark; the runner's status of ADR 0286 §7 is read from the grants part's account read, since a runner
that does not honour the mark is a version that cannot say so. A reading is tied to the runner that made it, not to
a time, so a clock or a late reading cannot lift the deny (step 0b). A call that reaches no responder because its
subject is withheld is answered by the mesh MCP server naming `withheld` and the grants part's read. *How it is
checked:* unit, the grants part's composition table: readings lacking the capability, no readings, readings with
another runner's fingerprint and readings with none each compose the deny of every subject above, the caller-named
ones, a marked seat verb's `.<node>` form and a mesh-wide seat's marked verb among them, each named in `withheld`;
readings with the capability and the running fingerprint compose no deny; the release's first composition passes
the user-list guard with the deny added; a node gaining `approval-mark/1`, where only its readings change, has the
deny removed; unit, the assigner's table: a reading keeps the announcing runner's fingerprint; whole (node-engine),
`composition` shows the withheld subjects, and with the grants part down says "withheld unknown"; integration, the
grants part's real-bus test: an account composed with the deny is refused delivery of a call on a marked subject
while it holds `mesh.mod.<module>.tool.>`, and so is a call sent through `process.Call` on the caller-named
subject.
7. **The asker's grants are a user's of `operator-channel`.** Because the manifest says `operator-channel` in `uses`
(§4), the grants part composes, on the account of the runtime carrying the module, the publish of
`mesh.seat.operator-channel.accept.ask.<module>` and `.cancel.<module>`, the hearing of its warrants on
`mesh.seat.operator-channel.event.decided.<module>` and the read of the router's record under `<module>.` in the
seat's state `asks`, as for every user (ADR 0259 §3). The runner passes the client its own publish and read on that
runtime's account (`Client.Publish`, `Client.Record`), never the bundle's. *Why:* ADR 0286 §6. *How it is checked:*
unit, a row in the grants part's composition table for a fixture module with a mark.
## The act, and the first call
8. **The act is digested with operator-channel's client.** For a call of a marked tool or verb, the tool runner
builds an `operatorchannelclient.Act` with `address` (as called), `node` (the node that serves it), `module` and
`tool`, or `seat` and `verb`, `build` (the fingerprint the bundle's environment carries, the version it serves, §5; Q-BD), and `arg.<name>` for each
argument given, a string as itself and any other value as its JSON in RFC 8785's canonical form; an argument left
out is not in the act. Its digest is `operatorchannelclient.ActDigest`. *Why:* ADR 0286 §10, §11; the digest is in
mesh-contracts since ADR 0627 §1. The TypeScript twin ADR 0286 named is not built: the tool runner is Go, and no
TypeScript asker computes an act. *How it is checked:* unit, rows in `approval` for the act's fields, an omitted
argument, a number, an object and a changed build each giving another digest.
9. **A call refused without asking.** On a call with no approval for its act, the runner refuses at once, asking
nothing, a call whose arguments hold anything `outward.CheckSecret` refuses (mesh-sdk), or whose arguments are more
than 16 KiB. *Why:* ADR 0286 §12; mesh-sdk's `outward` is the content rule ADR 0310 §4 moved there. *How it is
checked:* unit, two rows of §22's table.
10. **One ask, under the module's name.** Otherwise the runner asks through an `operatorchannelclient.Client` whose
`Module` is the marked module, whose `State` is the runner's state of §13, and whose new field `KeyPrefix`, which
the client in mesh-contracts gains and puts before every key of its own `State` it writes and reads (empty for
every asker today), is `<node>.`; never before the router's record it reads through `Record` and `Where`, whose key
stays `<module>.<id>` in the seat state `asks`; unless an ask for the same act is
open there (§13's `act.` key), in which case it asks nothing new. The ask: `about` the act's digest; `who`
`operator`; `expires` 60 minutes after; `on-expiry` "nothing runs; call again to ask anew", since the client's
`Check` refuses an authorising ask without it, and no option is taken on expiry (ADR 0286 §22; Q-OC2); two options, `approve` (label Approve) and `decline` (label
Decline), both at level `approve`, each binding the act's digest; the headline "Run <tool> on <node>?",
"Run <tool>?" when that is longer than the router allows (`HeadlineLength`, 60), and, when that too is longer,
the names cut with "…" to fit (Q-HL), ending "Part shown" whenever an argument is not shown
whole; the explanation: the first line of the tool's own description, the node, that it was called through the
mesh's tools, which cannot tell who called, every argument one per line as "name: value", and the act's
fingerprint, its digest's first 24 hexadecimal digits in groups of four. *Why:* ADR 0286 §13, §15, §16, §20 to
§23; the client checks the ask (`Check`) before it publishes. *How it is checked:* unit, a row in `approval` holds
the published ask's fields against a fixture call, and a second identical call publishes nothing.
11. **An argument is shown whole only where it may leave the mesh.** Each argument is shown whole where
`outward.Check` passes it, else in its shape with ‹path›, ‹address› or ‹withheld› in place of what it refuses; the
arguments are shown whole when they are at most 2000 bytes and 40 lines, otherwise their first 20 lines and 1200
bytes, marked "NOT SHOWN IN FULL", and the explanation then says that the whole is read with `mesh_approvals`
(§18) on that node, whose fingerprint must be the one on the phone. *Why:* ADR 0286 §17 to §19. *How it is
checked:* unit, rows of §10's test with a path, an address and a long argument.
12. **The first call answers "Not run", or that the router has not taken it.** The runner waits up to 5 seconds for
the router's record of the ask (§14) and answers the call as an error, never as a result. With the record: "Not
run: this needs the operator's approval. Asked on <channel> at <time> (ask <id>, fingerprint …); make the same call
again within 30 minutes of the approval; the ask expires at <time>." With no record within the 5 seconds: "Not
run, asked; not yet taken by the router (ask <id>, fingerprint …); make the same call again later." The ask stays
in the seat's stream of accepts, a work queue the router takes it from when it runs, and the `act.` key stays, so
the next call reads the router's record of the same ask and asks nothing new. A mark added since the caller last
called is named in the answer ("marked since <revision>"). *Why:* ADR 0286 §14, §37; the caller calls again, as
the operator chose on 2026-11-11 ("Call again (as designed)") over a held call that runs on the Approve and returns
its outcome (issue 417, 2026-10-16). *How it is checked:* unit,
rows of §22's table hold both answers; a row with no router holds that a second call asks nothing new; a row holds
a headline whose names are cut with "…" to 60 characters (Q-HL).
## The runner's record, and the same call again
13. **The runner keeps its asks and claims in a state of its own, per machine.** The tool runner, a foundation part,
declares a state `approvals` in its manifest's old `state` section, per machine, with a time to live of 172,800
seconds (48 hours), which the grants part creates as it creates a foundation part's states (ADR 0619 §2, §3; the bus
module refuses a per-machine state in the new form today). Its size is the old form's fixed one, 64 MiB
(`StateMaxBytes`), which §18 reads from the bucket, and a value is at most 256 KiB: an ask whose kept call would pass
it fails closed, naming the cause (ADR 0595 §3; Q-CAP). A runner whose membership binds no `approvals` serves every
unmarked call as before and refuses only marked ones (§19).
Its keys: `<node>.ask.<id>`, the asked call as the client keeps it (the ask, the act, the whole arguments, when
asked, whether acted on, and `recorded` once `warranted` answered, §16; the client's `Kept` gains `act`, `arguments`
and `recorded` for it, Q-KP), and `<node>.act.<digest>`, the id of the
newest ask for that act. The runner on each machine is the one writer of its machine's keys; a key is deleted by
its time to live, never by hand. A per-machine state reaches today only the key named for its machine; mesh-sdk's
per-machine form and the grants part's `stateAccess` gain the keys below it, `<node>.>`, for every per-machine
state, a bundle's membership (`stateIssuedFor`, ADR 0260 §18) too, in the same form: one more subject per
per-machine state on each account, and every module's per-machine keys keep their form; mesh-sdk's rebuild of it
rides in G's group. That size is the bucket's, shared by every machine's keys. A write the bus refuses (the size
reached, or any other) fails closed under §19 and is said in the answer; `mesh_approvals` reports the bucket's fill
against its size read from the bucket, saying that both are every machine's together. A run still unrecorded (§16) an hour before its key's 48 hours end is written in the runner's log
as an error naming the ask; this is written only while the runner runs, so a machine asleep through that hour keeps
only the `call-unanswered` history of §16, which is accepted.
*Why:* ADR 0286 §24, §30 kept the waiting call in the runner's record and claimed a warrant in the node's own file,
kept 48 hours, by one writer per node; the client built since keeps asks and marks a warrant acted on by
compare-and-set in a module's bus state (ADR 0259 §10, ADR 0627 §1), and a per-machine state (ADR 0595) is one
writer per machine, so the file is not built. The keys below the machine's own depart from ADR 0260 §16 (a
per-machine state reaches only its machine's key), the design's choice following the operator's answers of
2026-11-11 (the gate built in G); the key form is the design's choice too. *How it is
checked:* unit, the manifest test of `node-tools` holds the old-form entry (per machine, its time to live) and no
`approvals` in `requires`; a row of §22's table: an ask over 256 KiB refused naming the cause; another: with no
binding, an unmarked call runs and a marked one is refused; the grants part's composition table holds the
`<node>.>` reach and no other node's; integration, §22's whole test reads the keys.
14. **The same call again is judged on the router's record.** When a call of a marked tool comes and `<node>.act.`
names an ask, the runner reads the router's own record of that ask under the module's name (`Client.Where`, the
seat state `asks`, key `<module>.<id>`, whose value in operator-channel's definition gains the `warrant` it already
carries, so the definition names what `Where` reads; Q-OC1), never a record of its own or the caller's; it composes the ask again from
the act and holds the warrant to it with `operatorchannelclient.For` (the same asker and ask, the ask's digest, a
person's choice, an option offered, at its level, before it expired); it requires the option to be `approve` and
the act to be the one it bound (`operatorchannelclient.Performs`: the same node, build and every argument); and it
requires the call within 30 minutes of the choice. It judges the newest ask it keeps for the act. *Why:* ADR 0286
§26 to §29, §32. *How it is checked:* unit, rows of §22's table: approved, approved for other arguments, approved
31 minutes ago, declined, expired, another asker's warrant.
15. **One approval runs one call.** Before it runs, the runner marks `<node>.ask.<id>` acted on by compare-and-set
(`Client.Take`); a warrant marked before is refused, so one approval runs one call however often the call is made,
and a warrant replayed onto the bus runs nothing. After "already run", "expired" or "approved too long ago" the next
identical call asks anew, and the used mark stays. *Why:* ADR 0286 §30, §31, §33, §36. *How it is checked:* unit, a
row of §22's table makes the approved call twice and runs it once; two calls at the same moment run it once.
16. **The call runs, and its warrant is recorded, across a restart too.** Once the checks pass, the runner runs the
call and answers with the tool's own answer and a line naming the approval ("run on the operator's approval via
<channel> at <time>, ask <id>"). It then calls `mesh-hand-acts.warranted` with `asker` (the module) and `ask` (the
id) as a kept call in mesh-sdk's kept-call form (ADR 0668 §1, §2, §7; design 47 G §351, §425: the verb takes it once
per ask), the key that keeps it open being `<node>.ask.<id>` without `recorded`, and no event replacing it; it is not
`busretry`, which stops after 30 seconds (Q-KC). On its answer the runner writes `recorded` on `<node>.ask.<id>`. At each start the runner calls `warranted` again for every acted, unrecorded ask its keys hold,
within their 48 hours, each named in its `findings-stated`'s `pending` until it is made (design 47 G §384). One
unanswered after five minutes is the runner's finding `<node>.<ask id>.call-unanswered` in its state `findings`,
said on its stream `node-tools-findings` (design 47 G §383, §387), which the condition keeper hears; it clears when
`warranted` answers. *Why:* ADR 0286 §34, §35; the hand-act log is the seat `mesh-hand-acts`, whose `warranted`
reads the router's own record, and its "again every ten minutes for a week" is ADR 0668's kept call now, kept
across a restart by the `recorded` mark (ADR 0668 §3, ADR 0564). *How it is checked:* integration, §22's whole test
reads the hand-act recorded once, also when the runner is restarted after the run and before `warranted` answers;
unit, §18's row of `record`.
17. **What a later call is told.** For the same call made again before it can run, the caller is told, as an error:
still waiting; declined (the same act is not asked again for 10 minutes, and the answer says so); expired,
approved too long ago or already run (each asks again, §15); not yet taken by the router (§12); or refused, with the
router's words. An ask the router holds back as the asker's fourth open one is still waiting, and
`operator-channel.asks` lists it (ADR 0666 §12, as G builds it). *Why:* ADR 0286 §38, §39; ADR 0286 §40's refusal
of a fourth open ask was the router's before G, which now offers a fourth and holds the oldest back. *How it is
checked:* unit, rows of §22's table.
18. **The tools read and steer what was asked.** The mesh MCP server on each machine gains the tool `mesh_approvals`:
with no argument, the calls asked on that machine in the last day, newest first, each with its state and
fingerprint, read from §13's state, and the marks it applies, each module's marked tools and verbs with their
source (its module's entry in the catalog half or its seat's definition) and the catalog revision read; given an ask's id, its whole
arguments and the act's digest; given `record` and an ask's id, it asks `warranted` again for that one ask by hand,
in the kept-call form of §16, recorded in the hand-act log first; given `cancel` and an ask's id, it takes that open ask back (`Client.Cancel`),
recorded first in the hand-act log through `mesh-hand-acts.hand-act` with why, the verb taking who from the caller
it proves (Q-HA), and refused when the record
cannot be written. `mesh_describe` answers, for a marked tool or verb, `needs-approval` with its source. *Why:*
ADR 0286 §25 (the runner's `approvals`), named as the mesh MCP server's own tools are; a tool reads everything a
module holds and does by hand what it does by itself (the operator's instruction of 2026-10-25). *How it is
checked:* unit, rows in the mesh MCP server's tool table test: the list, the marks with their sources, an ask read
whole, a `record` that asks `warranted` again for one ask, a cancel recorded and one refused without a record, and
`mesh_describe` of a marked tool.
## Failing closed
19. **Nothing runs when a part of the check is missing, and a stuck gate is said.** A call of a marked tool is
refused, runs nothing and says why when: no module holds `operator-channel` (read from the assigner's first view
`mesh-delivery-assigner.views.seat-holders`, which the tool runner consumes; the seat list names no holders); the
served entry of the module cannot be read (§5), which refuses every marked call on that runtime; the
router refuses the ask; the ask cannot be published; the router's record cannot be read; §13's state cannot be read
or written, in which case no marked call runs on that machine until it can, the failed read or write being made
again in the kept-call form (ADR 0668 §7; design 47 G §351), kept open by the failing key, until it succeeds; or the marks are not yet read. Before
the first view of the catalog half is read, a call waits for it within its 5 seconds and is then judged, asking
nothing while the marks are unread; still unread, it is refused, naming `mesh_first_views`. When §13's state has
been unreadable or unwritable for five minutes, the runner raises its finding `<node>.approvals.approvals-unreadable`
(said on `node-tools-findings` as §16's), and clears it when the kept call succeeds; its `findings-stated` at a start
judges it anew from a read of the state (design 47 G §384). Each refusal is
written in the runner's log with the ask's id and fingerprint, never the arguments. *Why:* ADR 0286 §40, §42; a
gate that refuses every call for a reason nobody hears is a silent fault, and the finding and its five minutes are
the design's choice. *How it is checked:* unit, one row of §22's table per case, the holder read from a fixture
`views.seat-holders` with none and with one; a row where the served entry cannot be read and every marked call on
that runtime is refused; and a row where the state fails, the finding is raised after five
minutes, the state recovers with no further call, and the finding clears.
20. **The terminal is not exempt.** A marked tool called through `mesh-cli` or any caller asks as every call does: the
runner cannot tell who called. *Why:* ADR 0286 §41. The gate wraps every handler the runner serves, a bundle's
tools and seat verbs and the runner's own tools beside them (the ledger repairs) alike (Q-LR); the act, the ask,
the check, the claim, the recording and the refusals of §8 to §19 are all in `node-tools/internal/approval` (Q-PK).
*How it is checked:* the code review reads that the gate sits on every handler, before the bundle, and that no path
to a bundle or a runner's own tool passes around it; unit, a row of §22's table: a runner's own tool marked in a
fixture is refused unasked, never run.
## Marked first
21. **`node-login-shell.execute` is marked in its seat's definition.** mesh-contracts' `seats/node-login-shell/seat.json`
marks the verb `execute` with `needs-approval`; the zsh module's manifest (mesh-catalog `modules/zsh`), which holds
the seat, gains `operator-channel` in `uses`. The control-node keeps withholding `execute`, and whether it serves
it again under the mark stays the operator's setting. *Why:* ADR 0286 §44, §45: the reads it carried have their
tools on every node (the seat `node-reads` of issue 418, resolved 2026-10-12, which `node-files` replaces whole, ADR
0394 §9). *How it is checked:* live, as the acceptance says.
- **The forge's two acts on a trunk's protection are marked.** The forge module (mesh-catalog `modules/gitea`)
gains a tool `gitea_branch_protection_loosen`, marked in its `needs-approval`, taking `owner`, `repo`, `branch`,
`act` (`remove-protection` or `allow-force-push`) and `why` (Q-FG), which alone performs the two acts
`gitea_branch_protection_set_full` refuses today: removing a trunk's protection, and allowing a force-push onto
a trunk; `gitea_branch_protection_set_full` keeps refusing both and its refusal names the new tool. The manifest
gains `operator-channel` in `uses`. No other tool or verb is marked in this milestone. *Why:* the forge's own
refusal says these two acts are the operator's and waited for this mark (issue 417, 2026-10-28); a mark is on a
whole tool (ADR 0286 §1), so the acts move to a tool of their own, as ADR 0286 §48 splits a tool into an unmarked
and a marked form, and tightening a protection stays unasked; the operator, 2026-11-11: "Mark them in G". The
principle: loosening a trunk's protection runs only on the operator's own warrant for that one act, never on a
session's word. Its bound: a process that reads the forge module's own credential speaks to the forge directly
and goes around it, the same known limit as issue 1966 note 193; the prepare reads that credential's mode and
owner on the forge's node with `<node>/node-files.stat` (§21a) and writes them in the milestone file. *How it
is checked:* the forge module's tool test: `set_full` refuses both acts naming the new tool; the new tool
performs each; and live, as the acceptance says.
21a. **`node-files.stat` answers a mode under a directory the account cannot enter.** The seat `node-files`' holder
(mesh-catalog `modules/mesh-files`) answers `stat` of a path its account cannot reach through `sudo -n` with its
fixed `/usr/bin/stat`, as its writes already go (ADR 0394 §2), answering the path's mode, owner, group and kind and
nothing of its content; the secret places ADR 0394 §7 names are stated, never read. Its README's "not yet through
sudo" loses `stat`. *Why:* the prepare could not read the forge credential's mode (step 0b); ADR 0394 §2 has the
holder use sudo where it must. The lead's handover named the retired `node-reads` (ADR 0394 §9), whose successor this
is. *How it is checked:* unit, a row in mesh-files' `Stat` test: a file under a directory of mode 0700 owned by
another account answers its mode and owner through sudo, with no content.
## Tests, delivery and going back
22. **The tests.** The tool runner's tests in `node-tools/internal/approval` are one table-driven unit test of the
refusals and answers (§9, §12, §14, §15, §17, §19) and one of the marks (§5), on the tool runner's test bus
(`meshtest`), the router's record and the warrant written there as the router writes them; and one integration
test that starts the runner against the real bus, with no store, with a fixture marked module: a call, the
operator's Approve written as the router's record, the call again, the run, the hand-act, `mesh_approvals`, and a
restart between the run and `warranted`'s answer. The client's `KeyPrefix` (§10) has a row in mesh-contracts'
client test: every key of its own `State` written and read carries it, an empty prefix writes today's keys, and
with a prefix set `Where` still reads `<module>.<id>`. The mesh MCP server's `TestMeshWaitIsAnnouncedAmongSeven` counts eight
tools with `mesh_approvals` (Q-T). In milestone G a module ends with no more tests than it starts with (design 47 G §419, §626): these tests replace
existing ones, and every test deleted counts. *Why:* the operator's test
budget (one whole test over many small ones). *How it is checked:* the check counts each module's tests against
its budget.
23. **In G's delivery group, ordered by the module graph.** The members are pushed and checked in design 47 milestone
G's group, with design 34's milestone every-event. The orders are the module graph's: the indexer's manifest
validator and catalog graph entry that read `needs-approval` before any manifest or seat definition carrying it (a
reader edge, design 47 G §89, whose list covers `needs-approval`; Q-RE); and the grants part that composes the deny of §6 before the seat definition marking
`execute` (an edge of the seat definition's readers), so no machine serves `execute` unasked between the two; and
the grants part serving `withheld` before a node-engine calls it (a served-verb edge, §6).
*Why:* the operator, 2026-11-11, "Build the gate in G"; the operator, 2026-11-03, a change is designed as it should
be and its delivery is the delivery system's. *How it is checked:* the delivery plan names the three edges.
24. **Going back is going forward.** A fault met after the delivery is fixed by a new change delivered the same way.
Reverting instead reverts G's whole group in the reverse of §23's order: the seat definition's mark first, then the
grants part, then the validator. The `approvals` state is then declared by no running module: it is an orphan
`orphans` lists, its keys aging out within 48 hours, dropped through ADR 0595's `drop` on the operator's word.
*Why:* design 47 G's revert rule; ADR 0595 §4, §5. *How it is checked:* the delivery plan of a revert names the
order.
## Acceptance
- **A marked call is asked on the phone, does nothing, and runs once on the Approve** (live, read by tool call):
`node-login-shell.execute` on a node other than the control-node answers "Not run", the ask is on
`operator-channel.asks`, and after the operator's Approve the same call runs once, a third identical call is refused
as already run, and `mesh-hand-acts.hand-acts` names the warranted act (§10, §12, §14 to §16, §21).
- **The forge's loosening is asked, and the protection is put back** (live, read by tool call): on a scratch repository
`acceptance-approval-mark`, created by the acceptance through the forge's `gitea_create_repo` and kept afterwards,
never deleted, with full protection set on its trunk by `gitea_branch_protection_set_full`,
`gitea_branch_protection_loosen` removing the trunk's protection answers "Not run" and asks on the phone; after the
Approve the same call runs once; then `gitea_branch_protection_set_full` sets the full protection again and
`gitea_branch_protection_get` reads it back as it was (§21).
- **A dropped mark still asks while the old bundle runs** (live, read by tool call): a delivery that drops a module's
mark asks a call of that tool until the node runs the new bundle, read with `mesh_approvals` (§5).
- **No machine serves `execute` unasked** (live, read by tool call): before this acceptance passes, on every node
`execute` is either asked as above or absent, read with each node's `node-engine.composition` and
`mesh-grants.grants_user_list`'s `withheld` (§6).
- **A marked wait as the operator sees it** (live): design 34 every-event's acceptance of a wait on a mail event.
- **The mark cannot be lost on the way** (unit): §5's and §8's tables pass; a bundle carries no mark.
- **It fails closed** (unit): §19's rows pass.
- **The validator refuses a mark nothing enforces** (unit): §4's rows pass.
@@ -1,9 +1,12 @@
---
layer: to-be
status: in-progress
code: [mesh-controller, mesh-tools, mesh-sdk, mesh-catalog]
updated: 2026-10-13
code: [mesh-controller, mesh-tools, mesh-sdk, mesh-catalog, mesh-contracts, mesh-delivery]
updated: 2026-11-11
decisions:
- "ADR 0668"
- "ADR 0667"
- "ADR 0666"
- "ADR 0347"
- "ADR 0345"
- "ADR 0344"
@@ -15,6 +18,11 @@ decisions:
- "ADR 0253"
- "ADR 0234"
- "ADR 0227"
- "ADR 0627"
- "ADR 0597"
- "ADR 0595"
- "ADR 0310"
- "ADR 0268"
---
# 46 — The conversation with the operator
@@ -221,7 +229,20 @@ when the title and the note alone do not fit, and says that too.
### Limits, batching, history
- An asker holds **at most three open asks**; a fourth is refused in words.
- An asker holds **at most three open asks**; a fourth is refused in words. *Amended 2026-11-10, ADR 0666 §12, from the
delivery of design 47's milestone G:* **no ask is hidden, and the newest is never held back.** An asker still has at
most three asks offered at once, but a fourth is offered, not refused: the oldest of its offered asks is held back
instead, kept open with its answer tickets withdrawn, its message edited to say it waits behind newer asks of the
same asker, and offered again, with new tickets, as soon as one of the three ends. An ask with the same `about` as an
open one of the same asker replaces it (§10), so a held-back ask is never one a newer ask already stands for. `asks`
lists each asker's held-back asks with since when, and the router's tool `ask-again` offers one now, holding back the
oldest offered one in its place. A Decline ends an ask like any answer, and its asker may ask again at once: the
pipeline runner's verb `ask-again` asks a declined `approval` step so (design 47, *Approval is a step*). Why the
oldest: the newest is what the asker needs now, and the oldest has had the operator's eye longest; holding it back
edits its message rather than removing it, so the operator still sees it waits. *How it is checked:* the router's
whole-module test offers a fourth ask and holds back the oldest, offers the held-back one again when one ends,
replaces an open ask by a newer one with the same `about`, and offers an ask after a Decline; live, a fourth ask and
a declined approval each reach the operator (design 47, test 71).
- Asks count against the router's hourly cap, as messages do; answers do not.
- Asks to one channel within the burst window go out together under a heading ("3 questions waiting"),
each as its own message, answerable and editable alone.
@@ -424,7 +445,8 @@ what it does in plain words, its assurance level (`acknowledge`, `approve`, `des
answer (`operator`); an expiry; what happens on expiry, in words; and optionally what it is about (a
condition's key), so a newer ask about it replaces the older. It is refused at once, on its asker's
`decided` subject, when its words break a rule, when no ready channel can carry one of its options' levels,
or when the asker already has three open.
or, until the delivery of design 47's milestone G, when the asker already has three open; from that delivery a fourth
is offered and the asker's oldest held back (§6, *Limits, batching, history*; ADR 0666 §12).
**A warrant** carries the ask's id and asker, the option, `by` ("the operator, via telegram, user id
verified"), the channel module, the proofs, the level met and when. No secret. An ask that ends without one
@@ -545,43 +567,27 @@ said on the away channel, with its why.
### A call that needs the operator's approval
*Added 2026-10-10, ADR 0286.*
A module's manifest marks a tool, or a seat verb it serves, as **`needs-approval`**; a seat's definition may mark
a verb for every holder. The controller issues the mark in the runner's membership, never from the bundle, and
withholds a marked tool from a runner that does not declare `approval-mark/1`, by leaving its subject out of that
runner's bus permissions.
*Added 2026-10-10, ADR 0286; amended 2026-11-11 to the mesh as built and as design 47's milestone G leaves it.*
A module's manifest marks a tool, or a seat verb it serves, in **`needs-approval`**; a seat's definition may mark
a verb for every holder. The tool runner reads the marks from the catalog half on the bus (seat `mesh-catalog`), never from
the bundle, and on a machine whose tool runner does not state that it honours the mark the grants part withholds every
marked subject from its account.
```
agent ──call──► tool runner (serving node) ──ask, under the module's name──► router ──► Telegram
agent ──call──► tool runner (serving node) ──ask, under the module's name──► seat operator-channel ──► Telegram
▲ │ "Not run: asked, ask <id>" │ Approve
│ ▼ ▼
└──same call──► runner reads the router's record ◄────────────── warrant (decided.<module>)
│ Warrant.For + Option.Performs on the act; within 30 min; claimed once
│ For + Performs on the act; within 30 min; claimed once in its own state
▼
runs the call once ──► mesh-controller.warranted (hand-act log)
runs the call once ──► seat mesh-hand-acts, warranted
```
- **The act** is the address, the node, the module and tool, the bundle's build and every argument given, digested
with the shared library's `ActDigest` (in Go and TypeScript, one set of vectors).
- **The first call asks and does nothing.** One ask per act (its `about` is the act's digest): "Run <tool> on
<node>?", the tool's first line, every argument whole where it may leave the mesh or in its shape with
‹path›, ‹address› or ‹withheld› and "Part shown" (whole when at most 2000 bytes and 40 lines, else the
first 20 lines and 1200 bytes), the fingerprint, and where the whole is read (the runner's
`approvals` on that node). Approve and Decline, both at approve; expiry 60 minutes; never a default. The call
answers as an error: "Not run", with the ask's id, that the call runs within 30 minutes of the approval, and
when the ask expires.
- **The same call again** runs once on Approve, read from the router's record, within 30 minutes of the choice,
claimed in the node's own file (kept 48 hours, one writer per node); the runner judges the newest ask it keeps
for the act, and after "already run", "expired" or "approved too long ago" the next identical call asks anew;
then `warranted` records the hand-act. Anything else is
refused and said: waiting, declined (not asked again for 10 minutes), expired, already run, or the router's
refusal.
- **Fail closed:** a secret's shape in an argument or more than 16 KiB of arguments, no operator channel, a
fourth open ask, an unpublished ask, an unreadable router record, node file or mark: nothing runs.
- **The controller** enforces the same mark on its own verbs, asking as `mesh-controller`.
- **Marked first:** `node-login-shell.execute` on every node once the reads it carried have tools (issue 418).
`mesh-delivery.release` waits until a warrant the controller holds passes the mark of a verb it calls (issue
417), so a Release on a condition is not asked twice.
The first call asks and does nothing; the same call made again within 30 minutes of the operator's Approve runs once;
everything else is refused and said, and anything the check cannot read runs nothing. `node-login-shell.execute` is
marked first. Every line of it is in milestone 7's part,
[`46-milestones/7-approval-mark.md`](46-milestones/7-approval-mark.md), built and delivered with design 47's milestone
G (the operator, 2026-11-11).
### The hand-act
@@ -696,12 +702,13 @@ 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 |
| **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 list; `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 list, 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 |
| **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 |
| **6 — Telegram live** | the `telegram` module (built in 5a) with replies and forced-reply codes, under its own account on the control node; the operator's two bots configured; the watcher assigned off the control node; the dead-man ping | mesh-catalog | the self-check says the away channel carries every tier; rehearsals: an approve and a destroy answered on the phone, with the hand-acts read |
| **7 — A call that needs approval** (ADR 0286) | `needs-approval` in the manifest and the seat set, issued in the membership, withheld from a runner without `approval-mark/1` in its bus permissions (controller); the act's digest in TypeScript beside Go, with shared vectors (shared library); the check, the ask, the runner's `approvals`, the node's used file and `warranted` (tool runner); the controller's own verbs; `execute` marked (the controller's seat set) once issue 418's reads exist | mesh-controller, mesh-sdk, mesh-tools, mesh-catalog | a marked call on a node is asked on the phone, does nothing, runs once when made again after Approve, is refused the third time and asked again, and `hand-acts` shows the warrant |
| **7 — A call that needs approval** (ADR 0286), delivered with design 47's milestone G | the mark in the manifest and the seat definition, read by the tool runner from the bus; the manifest validator's refusals; the capability `approval-mark/1`; the act, the ask, the check, the claim in the runner's own state, `warranted` and `mesh_approvals`; `execute` marked. Its part is [`46-milestones/7-approval-mark.md`](46-milestones/7-approval-mark.md) | mesh-sdk, mesh-contracts, mesh-delivery, mesh-tools, mesh-catalog | its part's acceptance |
| **8 — No ask hidden** (ADR 0666 §12), delivered with design 47's milestone G | a fourth ask offered and the asker's oldest held back, offered again when one ends; `asks` lists the held-back asks; the router's tool `ask-again`; the pipeline runner's `approval` asks and their `about` | mesh-catalog (the router) | a fourth ask and a declined approval each reach the operator; `asks` shows the held-back one |
**Switching phase 5a on: the operator's steps, in order** (amended 2026-10-09, ADR 0259). Each line is typed with
`nox` (`mesh-cli`) on the control-node, from the operator's own account, which is the controller's terminal
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,419 @@
---
layer: to-be
status: in-progress
design: 47
milestone: G
updated: 2026-11-11
decisions:
- "ADR 0668"
- "ADR 0667"
- "ADR 0666"
- "ADR 0664"
- "ADR 0661"
- "ADR 0656"
- "ADR 0633"
- "ADR 0619"
- "ADR 0614"
- "ADR 0596"
- "ADR 0595"
- "ADR 0593"
- "ADR 0584"
- "ADR 0573"
- "ADR 0567"
- "ADR 0564"
- "ADR 0548"
- "ADR 0448"
- "ADR 0429"
- "ADR 0398"
- "ADR 0363"
- "ADR 0283"
---
# 47, milestone G: pipelines per module: registration, watched branches, steps and policy, what holds a delivery, approval, groups, dependencies, a publish
One file of milestone G's part of [design 47](../47-delivery-from-commit-to-delivered.md); the part opens at
[G.md](G.md). Its design lines are numbered across every file of the part and cited as "design 47 G §n".
## Pipelines per module
*ADR 0666, ADR 0667; ADR 0595, ADR 0596, ADR 0448 §2, §4, ADR 0548 §5, ADR 0564 §2, §4, ADR 0283, ADR 0656 §4, §6, ADR 0633 §5,
ADR 0664 §4, ADR 0398 §2, ADR 0593 §9; research 086, Direction.*
§1 From milestone G's delivery. A change of any module, the delivery system's own parts included, goes from a commit to
the machines through that module's **pipeline**: the pipeline steps its **policy** names, run for each commit or group
on each of the module's **watched branches** by the seat `mesh-pipeline-runner`, ordered by the catalog graph's edges
between artifacts, with no hand act (ADR 0666 §1). Milestone G is the one milestone that builds it, beside the two
graphs, the revision view and the delivery record (ADR 0667 §1). Until G's delivery the sections this one replaces
(*Checks*, *A try*, *The Approve and the merge*, the state table and the trunk-only want) hold as written, and they
check and deliver milestone G itself (ADR 0667 §2); each of them says so in a line of its own.
```
a session ──merge-pull (seat git)──► forge ──commit on a watched branch──► forge bridge ──► indexer (mesh-catalog)
│ registrations; a catalog graph
│ per watched branch; the revision
│ validated before take-in
▼ take-in.taken
pipeline runner (mesh-pipeline-runner) ── asks each pipeline step on the serving seat's work queue, hears step-ended:
│ lint, unit tests, integration tests ──► the build seats (node-build-go, node-build-node, node-build-image)
│ composition ──────────────────────────► the assigner (mesh-wants)
│ code review ──────────────────────────► the runner's own verb `review`, from the reviewer's bus account
│ approval ─────────────────────────────► operator-channel: accepted kind ask, then decided
▼ pipeline-ended (every step's result), keyed by source revision
assigner ──► wants from a revision whose required steps passed, on the watched branch a placement chose
gate ──► the delivery record keeps the step and pipeline events
```
### A module's registration
- §2 **What it holds.** The seat `mesh-catalog` keeps each module's **registration**: its git origin, the path of its
manifest, its watched branches and its policy, on the bus, every version with who changed it and the warrant of the
operator's Approve (ADR 0666 §2). The indexer is a foundation part, so the registrations are kept in its old sections
(ADR 0619 §1 to §3, ADR 0666 §4): one key per module in its old `state` section `registrations`, and every version on
its old `streams` section's stream `registration-history`, kept with no age limit.
The catalog graph is derived from the registrations and the manifests they point to; the mesh knows modules, and a
repository only as a module's git origin.
- §3 **Changing one is a trusted act.** The seat's verbs `register` (a module whole), `policy` (one module's step
statuses, per artifact), `watch` and `unwatch` (add or remove a watched branch) each raise an authorising ask of tier
approve through the seat `operator-channel` (design 46 §10), naming every value the change moves, one line each
("`mesh-grants:program` unit tests: `required` → `bypass`"), and write the version only on its warrant; a Decline or
an expiry writes nothing (ADR 0666 §2). Until a record binds each forge account to its bus identity, `register` and
`policy` refuse code review as a `required` step, naming why, so no review gates while its author cannot be told
(ADR 0666 §22; issue 1966, note 157). A manifest moved to another path is the one manifest change that changes the
registration; any other manifest change is a commit, judged at its revision.
- §4 **Said and read.** Each written change is said as the seat's event `registration-changed` (the module, the version,
what moved); the verb `registrations` answers every registration, or one with its history. The indexer derives the
modules it keeps from them, and the forge bridge its watched branches, each by a first view of `registrations` and
then that event, never by reading the ledger (ADR 0666 §3, ADR 0596).
- §5 **An unprotected branch is refused.** `register` and `watch` read a branch's protection through the seat `git`'s read
verb `branch` (its head, and whether a rule refuses a direct push and admits only merges), and refuse a watched branch
the forge does not protect, naming it (ADR 0666 §3).
- §6 **The foundation parts are the manifest validator's list.** The manifest validator's list (ADR 0619 §3) holds
exactly the foundation parts, ADR 0619 §1's, the runner, which joins them (ADR 0666 §4), and the forge and image
registry modules (§584); the checker leaves the
list when it is removed (*How milestone G itself is checked and delivered*). A registration holds no foundation
value. How it is checked: a unit test of the manifest validator refuses a template in the manifest of every part on
its list, the runner's included, and judges every other part's manifest as any part's.
- §7 **No part fixes a name: a guideline the code review reads.** (§507) Every branch, repository and module name a part
needs is read from the registrations and the catalog graph, never fixed in its code. This is a guideline, not a
step: the code review reads it, and no lint step scans for names (the operator, 2026-11-11 about 00:05 CET: the scan was a flaky check; where
guidelines live is research 087's). It departs from ADR 0666 §5's refusal by the lint step, by the operator's choice,
and a later record brings it in line. The names G's own work removes (about 39 in 9 parts, found on 2026-11-09,
research 086, Findings) are removed by its subtasks, each naming the name it removes.
- §8 **The first registrations are one ask.** At its first start on G's build the indexer, holding no registration,
proposes one registration for every module its trunk revision holds, from what it took in (the origin and path of
each), its watched branch the repository's default branch read through the seat `git`, and the statuses the check
judged until then: lint, unit tests, integration tests and composition `required`, code review `optional`, approval
`bypass`. Each step the artifact's build seat answers `steps` with none is `bypass` instead, so no module is held by
a `required` step it has nothing for; the indexer asks `steps` of each build seat as a kept call (ADR 0668 §1, §2),
and the seed waits until every build seat has answered, never reading an unanswered seat as "none": a seat still
unanswered after five minutes is the indexer's `call-unanswered`, naming the seat (ADR 0668 §3), and the seat's new
build serving `steps` is an edge of kind 6 before the seed. A module whose default branch the forge does not protect
is left out of the seed and named in the ask, as `register` would refuse it (ADR 0666 §3). It raises the rest as one
ask naming every registration, as the seat `mesh-module-assigner`'s `adopt` proposes placement whole (ADR 0567).
While the ask is open the indexer's finding `seed-waits`, naming the ask, is raised by the condition keeper as
waiting on the operator (ADR 0283) and cleared by the warrant; a Decline or an expiry leaves the seed unwritten, says
so and keeps `seed-waits`, naming `indexer_seed_again`, which raises the ask again, recorded in the hand-act log.
Until that Approve the runner starts no delivering pipeline and says why, so the operator is told of this one ask
before G's delivery starts. Why: the design's choices under ADR 0666 §3, §7 and ADR 0668 (round 1 of the review by
architectural domain, 2026-11-10). How it is checked: whole (indexer), a build seat that never answers `steps`
holds the seed and raises `call-unanswered`, one that answers none gives `bypass`; an unprotected default branch is
left out and named; `seed-waits` is raised while the ask is open and cleared by the warrant.
- §9 **Kept for the life of the mesh**, never derived again (research 086, choice 25): the history stream's cap is 64 MiB.
About 120 registrations of about 1 KiB, and an estimated change a week each (a guess, not a measure), grow about 6 MiB
a year, so the cap holds about ten years; past 80 % the indexer says `filling`, as every store of *Two graphs, one
source of truth* does.
- §10 **How it is checked.** Whole (indexer): an unprotected watched branch is refused, naming it; a policy change without
the warrant writes nothing, and a declined one writes nothing; a change from `required` to `bypass` asks, showing
that line; `registration-changed` is said and `registrations` answers the history; the first start proposes the seed
as one ask, and `indexer_seed_again` asks it again after a Decline and after an expiry; a policy making code review
`required` is refused. Tests 75, 68 and 83.
### Watched branches and the catalog graph per branch
- §11 **Only a watched branch starts a pipeline.** (§509) The forge bridge hears every commit of every watched branch the
registrations name, through the seat `git`'s event `branch-moved` (a branch's new head, from the forge's push
webhook, which a merge and an admitted push alike say), and takes the commit from the forge's own record by the seat's
verb `branch`, never from the event's body (ADR 0666 §6). One event for every way a branch moves, because a merge is
not the only one. `branch-moved` is kept on the seat's stream `SEAT_git_branch-moved`, which the bridge hears through
a durable consumer, so a move said while the bridge is down is heard when it starts; at its start it also reads each
watched branch's head the same way.
- §12 **A head with no pipeline is a finding.** (§497) A watched branch's head that starts no pipeline within five minutes is the
watchman's finding `pipeline-stands`, naming the branch and its head, raised by the condition keeper (ADR 0564 §2),
unless it waits as progress not owed: a member of a gathered group is governed by ADR 0633 §5's 30-minute bound, and
a merge behind a pending revision of the same branch by its 60-minute bound (ADR 0666 §24). The finding clears on the
runner's `pipeline-started` for that head.
- §13 **The catalog graph per watched branch.** The indexer keeps the catalog graph once per watched branch, from that
branch's manifests at each source revision (ADR 0666 §6): each revision names its branch, and the stores, caps and
pins of *Two graphs, one source of truth* hold per branch.
- §14 **A placement names the branch.** Each module of a placement key names the watched branch its node runs, by default the
registration's first, set by a placement proposal like every other placement change (ADR 0567, ADR 0666 §6). The
assigner computes each node's entries from the catalog graph of the branch its placement names, so a module watched on
`main` and `beta` runs `beta` only where a placement chose it.
- §15 **A source revision per watched branch.** The gate keeps one source revision per watched branch, `source.<branch>`,
in place of `source.main` (ADR 0429 §11 to §13 as ADR 0666 §6 changes them), in the old `state` section `source`
already holds, its key's layout being the only change. The gate is no foundation part: the bus module's delivery
waits on no act of the gate's, since the indexer takes a revision in from the forge bridge's stream and the assigner
writes wants from the runner's events, so the gate's kept data are as §613 has them, state by state (ADR 0666 §4), and serves it by the seat
`mesh-delivery`'s verb `source` with the branch, saying `source-moved` on each new revision; the indexer and the
node-engine's catch-up read it there, never the gate's state (ADR 0596). Each want names a build-input fingerprint
computed at one such revision.
- §16 **Validated before any take-in, on every branch.** Before it takes a revision in, on every watched branch, the running
indexer asks the manifest validator built from the revision's heads to judge it in the build seat's sandbox (the
ask `run`, *What ADR 0656 leaves to this design*). A revision it refuses stays pending under ADR 0633 §5's
60-minute bound, its hand acts and its condition `take-in-waits`; the way back is a revert, an ordinary merge
(ADR 0666 §6, §26). A revision on the branch the indexer's own placement chose is taken in only by the indexer it
names (ADR 0633 §1). This validation is no pipeline step and waits on no pipeline, so every delivering pipeline runs
on a revision already taken in.
- §17 **A branch whose protection was removed starts nothing.** (§497, §509) On every `branch-moved` the forge bridge reads the
branch's protection again; a move of a watched branch whose protection was removed starts no delivering pipeline, and
the forge bridge writes the finding `branch-unprotected`, naming the branch, which the condition keeper raises and
clears once the protection is back (ADR 0666 §6).
- §18 **A pending revision holds its own branch only.** A merge waits behind a pending revision only on the same watched
branch; other branches move on (ADR 0666 §6, which changes ADR 0633 §3).
- §19 **A refused new indexer holds its branch until a revert lands.** A merge of a new indexer whose own validator refuses
its revision has already landed when it is refused, since the validation runs before take-in and after the merge
(ADR 0666 §6). It stays pending, and every later merge on that watched branch waits behind it, said by
`take-in-waits` at the 60-minute bound, until a revert, an ordinary merge through its pipeline (ADR 0666 §26), lands
and is taken in; nothing takes a later merge in past it. This is the cost of validating every revision with the
validator built from its own heads, the design's reading of ADR 0666 §6, and the operator is told it with the
condition, which names the pending revision and the revert as the way on.
- §20 **How it is checked.** Whole (forge bridge): a commit of a branch no registration names starts nothing; a commit on a
watched branch is taken from the seat's `branch` read, not the event; a head missed while the bridge was stopped is
read at its start. Whole (indexer, assigner): a commit on `beta` gives a revision of the `beta` graph only and moves
only the want of the node whose placement chose `beta`; a revision whose own validator refuses it stays pending and
holds `beta` while `main` moves on. Whole (watchman): a head with no pipeline for five minutes raises
`pipeline-stands`, cleared by `pipeline-started` for that head, and a gathered member does not; a move said while the
bridge was stopped is heard from the seat's stream. Tests 64, 68, 76, 78 and 82.
### Pipeline steps and the policy
- §21 **The runner.** The seat `mesh-pipeline-runner`, held once for the mesh by the module `mesh-delivery-pipeline`,
runs each pipeline (ADR 0666 §7). It is a foundation part, since the bus module's own delivery waits on its pipeline
(ADR 0666 §4): its kept data are in old sections (ADR 0619 §2), it waits on no bus module process to start, and it
joins the manifest validator's list of foundation parts, which refuses its manifest asking a template (ADR 0666 §4,
which changes ADR 0619 §1 and §3). It reads the policy from the registrations, asks each step that is not `bypass` of
the seat that serves it, and says each step's start and end and the pipeline's end (*Health and going back*, below).
- §22 **The steps and who serves them.** Lint, unit tests and integration tests are the build seats' (`node-build-go`,
`node-build-node`, `node-build-image`): each gains the verb `steps`, which answers which pipeline steps an artifact
can run by its language's convention (as *The check from release 4* says the tests are found), and the accepted kind
`step` on its stream `check`, which runs one. Composition is the assigner's, an accepted kind `step` of the seat
`mesh-wants`. Code review comes in through the runner's own verb `review`. Approval is asked as the seat
`operator-channel`'s accepted kind `ask`, on its work queue by the runner's name (design 46 §10, *Asks, warrants and answers on the bus*;
the seat has no verb `ask`), and heard on its event `decided` (ADR 0666 §7, §10). A step with nothing to do ends at once as
`nothing to run`, never as passed.
- §23 **The policy.** (§553) Each step's status, per module and per artifact, is `required` (runs; holds the delivery until it
passes), `optional` (runs; a failure is shown and may be accepted) or `bypass` (nothing runs; said as bypassed),
kept in the registration and never in a manifest: the manifest validator refuses a manifest that names a status
(ADR 0666 §7; research 086, R3). A `required` step that ends `nothing to run` holds the delivery and names the step.
- §24 **Asked on a work queue, ended by an event.** The runner asks each step on the serving seat's work queue and hears
its end as that seat's event `step-ended` (the record's `step ended`), on
`mesh.seat.<seat>.event.step-ended.<node>`, the node that ran it in the subject and each holder's account granted only
its own node's subject, through a durable consumer of its own, so a step outlives a restart of either side (ADR 0666
§7, ADR 0363 §2). A build seat's holder restarted mid-step gets the step again from its work queue and finishes it
(ADR 0664 §1). A unit or integration test step says its catalog module's test counts in `step-ended`; `pipeline-ended` carries each
catalog module's counts against the test budget, which is counted per catalog module (at most 50 integration and
500 unit tests each), and `pipeline-ended` names any catalog module over it (ADR 0666 §8; the test budget, per
module, the operator 2026-11-11). A step's outcome is one of `passed`, `failed`, `nothing to run`,
`bypassed`, `carried` (with the pipeline it was carried from), `could not run` (naming the part not heard),
`waiting on <person>`, `accepted` (a failed `optional` step a person accepted, with who and why), `not asked` (an
`approval` step after a failed `required` step), `superseded` and `cancelled`.
- §25 **Carried, not run.** A step whose build-input fingerprint and policy did not change is carried by its package
digests, as ADR 0548 §5 holds, the runner making the carry instead of the checker: a test step runs only the packages
with no kept result for their digest, and a run on request's results are reused when the merged commit's inputs match
(ADR 0666 §8; research 086, choice 24). An `approval` step is never carried.
- §26 **Approval comes last.** The runner asks the `approval` step only once every other `required` step of the pipeline
passed, so the operator approves a commit the mesh already judged, the one place ADR 0661 §3 had the check gate the
ask. A failure after an approval is said, as a failed `optional` step is, and the approval stays.
- §27 **Kept on the bus, in old sections.** The runner keeps each pipeline's state in its old `state` section `pipelines`,
one key per pipeline keyed by its watched branch, source revision and module, written by compare-and-set so a holder
replaced mid-pipeline and its successor never both write one; its step log on its old `streams` section's stream
`step-log`; each step's output in its old `objects` section `step-outputs`, named in the log; and each package's
passing result per level in its old `state` section `kept`, under the digest of what its tests read (ADR 0666 §4, §8;
*The runner's kept data*, below). `kept` starts empty: the runner takes no verdict of the checker's, since a step's
result is taken only from the bus account of that step's seat holder (ADR 0666 §21), so the first pipelines after G
run each package's tests once and carry from then on (the design's choice, 2026-11-10).
- §28 **How it is checked.** Whole (runner): a step with unchanged fingerprint and policy is carried and says from where,
and an `approval` step is not; a module with no integration tests shows `nothing to run`, and a `required` unit-test
step on a module with no tests holds its delivery; a bypassed step is said; a runner restarted mid-step hears the
step's end and finishes the pipeline; two runners writing one pipeline key, one loses by compare-and-set; a test step's
counts are in `step-ended` and a catalog module over budget is named in `pipeline-ended` (§419). Whole (Go build module): `steps`
answers the steps of a fixture artifact, a `step` ask ends with `step-ended` on its own node's subject, one on another
node's subject is refused by the bus, and a build module restarted mid-step finishes it. Unit (manifest validator): a manifest naming a status is refused. Tests 65, 66, 73
and 88.
### What holds a delivery
- §29 **The assigner writes a want only from a passed pipeline.** The runner says each pipeline's end as `pipeline-ended`
(the record's `pipeline ended`), keyed by source revision, with every step's result. The assigner consumes it, and
writes a want only from a source revision whose every `required` step passed for the modules it moves, on the watched
branch a placement chose, by compare-and-set on a newer revision, so an older revision ending after a newer one
writes nothing (ADR 0666 §9). The compare-and-set is keyed per module and watched branch: it compares a revision only
with the last one written for the same module on the same watched branch, so a revision of `beta` ending after a
newer one of `main` writes its own want and refuses neither; a later `pipeline-ended` of the same revision (after a
passing `rerun`, below) is taken, since its end count is higher (the design's choice under ADR 0666 §9: source
revisions are kept per watched branch, `source.<branch>`, and two branches' revisions have no order between them). This is one more rule beside the assigner's others (ADR 0429 §6), checked per module.
From G's delivery it replaces the trunk-only want (ADR 0656 §2 as ADR 0666 §9 changes it). For a group's source
revision the runner says one `group-ended` once every member's modules have ended, and the assigner writes the
group's wants only on it: a member's own `pipeline-ended` writes nothing (ADR 0666 §9).
- §30 **A failed `optional` step holds nothing.** It is shown in `pipeline-ended`, in the delivery record and in the runner's
`show`; the operator or a session may accept it by the runner's verb `accept` (§394, §610), recorded in the hand-act log with who and why.
- §31 **A newer commit supersedes an older pipeline of a module whose fingerprint it changed.** On a newer commit of the
same watched branch the runner ends a module's older running pipeline as `superseded` only when the newer commit
changed that module's build-input fingerprint (ADR 0666 §9); a module the commit leaves as it was finishes its
pipeline. A newer publish of a package likewise supersedes the rebuild pipelines an older publish of it started. A
superseded pipeline's waiting asks are dropped by each build seat's `drop` (ADR 0664 §4), naming
the newer commit as the cause, so a cancelled pipeline leaves no ask on a stream (a gap G's prepare found: a cancelled
build withdrew only the builder's wait).
- §32 **A step nothing starts ends its pipeline.** A step whose prerequisite nothing starts (a seat with no holder, a
prerequisite step `bypass`) ends the pipeline refused within five minutes, naming the step and what it waits for,
never as a silent wait (research 086, R5); a step whose seat has no holder ends its pipeline failed, naming the seat
(ADR 0666 §17).
- §33 **A failed head is said to its author, then to the operator.** (§540) A `required` step that failed, or ended `could not
run`, on a watched branch's head is the runner's finding `pipeline-failed`, naming the module, the branch, the commit
and the step; the condition keeper raises it as the commit's author's for two hours and then the operator's, as ADR
0505 §5 and §6 have a delivery whose cure is its author's new head, and clears it when a newer revision of that branch
passes that step (ADR 0666 §24). The author is the forge's record of the commit, read through the seat `git`.
- §34 **The delivery record keeps every step.** The gate's delivery record consumes the step and pipeline events and keeps
them as long as the record (ADR 0666 §9; *The delivery record*).
- §35 **How it is checked.** Whole (assigner): a failed `required` step writes no want; a failed `optional` step holds
nothing; an older revision's `pipeline-ended` after a newer one's writes nothing, and a revision of one watched branch
ending after a newer one of another writes its own want; a group with one member passed and
another still running writes no want until `group-ended`. Whole (runner): a newer commit that changed a module's
fingerprint supersedes its older pipeline and drops its asks, and one that left it unchanged lets it finish; a step
with no holder ends the pipeline naming the seat within five minutes; a failed `required` step on a head raises
`pipeline-failed`, the author's for two hours and then the operator's, cleared by a newer passing revision. Tests 66,
67, 87, 89 and 98.
### Approval is a step, and a session merges a pull request
- §36 **No Approve per merge.** The operator's approval is the pipeline step `approval`, its status in each module's policy;
no Approve is asked per merge, and the delivery gate's merge turns and its Approve per merge go (ADR 0666 §10, which
changes ADR 0661 §3 to §5). The operator is asked only where a module's policy says `approval`, and for registrations
and policy changes.
- §37 **The approval is an ask of the runner's.** The runner raises an authorising ask of tier approve as the seat
`operator-channel`'s accepted kind `ask`, under its own name, its `about` the module and the watched branch only, so a newer commit's ask
replaces the older one's (design 46 §10). The warrant ends the step passed; an ask that is declined, expires or is
refused ends it `declined`, and one that is replaced ends it `superseded`. Only a declined `approval` step is asked
again, by the runner's verb `ask-again` (ADR 0666 §10). While it waits the runner
says `wait-held` waiting on the operator, never a stall (ADR 0283).
- §38 **A session merges a pull request.** Any session the tool runner proves, and the operator, merges by calling the seat
`git`'s verb `merge-pull` over the mesh, which merges with the forge tools' account (ADR 0398 §2) at the head the
caller names. It judges that this is still the pull request's head and that the branch's protection refuses a direct
push, the protection read again on every `branch-moved` of the branch, and it judges the head's runs by its modules'
policies: it refuses a head when any run at its build-input fingerprint failed a step a policy marks `required`, and
merges a head whose `required` steps passed or that has no run yet, a failed `optional` or a `bypass` step refusing
nothing; it never waits for a run and never asks an Approve (ADR 0666 §11). The forge keeps only its branch protection.
- §39 **Judged from its own state, never by a call.** (§487) The holder of the seat `git` consumes the runner's `step-ended`
through a durable consumer of its own and keeps, in its own state `failed-steps`, each failed `required` step by
module and build-input fingerprint, with the commits it was seen at and the run that failed it, until none of those
commits is a watched branch's head or the head an open pull request names (*The runner's kept data*); so a head
merges while the runner is down, judged on what it already heard (ADR 0666 §11, ADR 0596). A head's fingerprints are
those its runs named for its commit (kept as §475 says); a step that failed at that fingerprint in any run, a group's or another commit's
of the same content included, refuses it, unless a later run of that same step at that fingerprint, asked again by
the operator or the healers (the runner's `rerun`), passed: that pass clears the failure from `failed-steps` (ADR 0666
§11). The runner's `step-ended` says that the step was a rerun and the caller the runtime proved, and the forge module
clears a failure only for a rerun whose caller is the operator or a healer; a plain pass, or a rerun asked by anyone
else, clears nothing. A run still running is judged by the steps it has ended, since the merge never waits. A refusal names the run, the module and the step, so the session knows what to fix. The gate merges nothing, and its forge merge token is
removed with the checker's buckets and bus account, after the first pipeline after G's delivery has delivered a change
end to end and on the operator's word (ADR 0667 §12; *How milestone G itself is checked and delivered*); the forge bridge hears the landed commit as it hears every commit of a watched
branch.
- §40 **The operator-channel hides no ask.** It keeps design 46's three open asks per asker, never holds the newest back,
and replaces an open ask by the same asker's ask with the same `about` (ADR 0666 §12; design 46 §6).
- §41 **How it is checked.** Whole (forge module, forge bridge): a session's `merge-pull` at its head merges with no run yet,
and with a passed one, and asks no Approve; with the runner stopped a head merges; a head at whose fingerprint any
run failed a `required` step, a head that moved and a branch whose protection was removed since its last move are
refused, each naming why; a failed `optional` step refuses nothing. Whole (runner): an `approval` step is asked last,
a Decline and an expiry each end it `declined` and `ask-again` asks it once more, and a newer commit's ask replaces the
older and ends its step `superseded`.
Live: a declined approval and a fourth ask each reach the operator. Tests 71 and 91.
### Groups
- §42 **Formed from the branch name.** The gate forms a group from a shared branch name carrying its issue number, as it
does today without the fixed list of trunk names (ADR 0666 §13).
- §43 **A member reaches its group by landing.** A member has reached its group when its commit lands on its modules'
watched branch. The forge bridge sets the group on each landed member's commit, reading the members from the seat
`mesh-delivery`'s verb `groups`, and publishes `forge.group.merged` when that verb says every member has (ADR 0666 §13,
which changes ADR 0633 §4).
- §44 **One source revision, one pipeline per branch.** The indexer keeps landed members pending until the group's end and
takes them in together as one source revision of that watched branch, saying `take-in.taken`; the runner starts the
group's one pipeline per watched branch on that event, and a member of a gathered group starts no pipeline of its own
(ADR 0666 §13).
When one member's `required` step fails, none of the group's modules is delivered (research 086, R5).
- §45 **A group that never completes is said.** By ADR 0633 §5's 30-minute bound and its condition; `bridge_group_end_again`
says a group's end again from the gate's group verb, never the gate's `merges` state (ADR 0666 §13, which changes ADR
0633 §5).
- §46 **How it is checked.** Whole (indexer, gate, forge bridge): a group of three repositories, its own packages included,
is one source revision and one pipeline; one member's failed `required` step delivers none; a group with a member that
never lands raises `take-in-waits` at 30 minutes. Tests 67 and 81.
### Dependencies between artifacts
- §47 **Six kinds of edge between artifact references.** The catalog graph's edges run between **artifact references**,
`<module>:<artifact>`, such as `mesh-grants:program`, of six kinds (ADR 0666 §14):
| Kind | B before A | Derived from |
|---|---|---|
| 1 | B published before A builds | a package A's build source imports (ADR 0573 §3) |
| 2 | B published before A delivers | an artifact A's manifest fetches by reference at its start |
| 3 | B delivered before A builds | a build module A is built by, and the toolchain its agreed version names (ADR 0584 §1) |
| 4 | B delivered before A delivers | a provision A requires and B provides, bus state among them (ADR 0614 §5), and a format's reader (below) |
| 5 | B has acted for A's new version | a seat's acting rule (*What ADR 0656 leaves to this design*): a grant, a credential, a verdict, bus state |
| 6 | B runs a version that serves A | a verb or a manifest word A uses and B serves or reads (ADR 0614 §2, §4), the build seats' `kept-artifact` among them |
Each kind is derived from what manifests and seat definitions already say, and declared per artifact in the manifest's
artifact entry, its field `needs`, only where nothing else says it. Today's edges run between modules, and only a
package edge names an artifact; at module level the tool runner and the grants part looked like a cycle, per artifact
they are a chain (`mesh-grants:program`, then `node-tools:runtime`, then `mesh-grants:tools`).
- §48 **Removal runs the other way.** When something goes away the order is reversed: a seat moved to a new holder has its
old holder released before the new one claims (ADR 0666 §14).
- §49 **The edges alone order.** (§569) The build and the delivery order by these edges alone; a cycle of delivery edges
(kinds 2, 4, 5, 6) alone is delivered as one step, and a cycle with any build edge (kind 1 or 3), whatever its other
edges, is refused, naming every artifact on it (ADR 0666 §14; research 086, R17). The cycle rule is the indexer's, in the catalog graph it keeps, so the checking
program's cycle rule goes with the checker.
- §50 **A format's reader first.** A format a running reader cannot read is preceded by its reader, an edge of kind 4 on
every watched branch (ADR 0666 §14): the indexer's manifest validator for a new field of a seat definition or a
manifest, and on each node the node-engine that reads a catalog graph entry's new fields before any manifest that writes them
(ADR 0667 §4).
- §51 **How it is checked.** Unit (indexer's graph): a chain per artifact where modules would cycle; two programs needing
each other's new verb are one step; two artifacts each needing the other's new package to build are refused, naming
both; a seat moved to a new holder releases the old first. Live: the tool runner and the grants part delivered in
the order `mesh-grants:program`, `node-tools:runtime`, `mesh-grants:tools`. Test 79.
### A publish rebuilds
- §52 **A version is reserved before the build and released when every artifact is published.** A module's version
is its manifest's `version`, `MAJOR.MINOR`, and a patch the build system gives: the next after the highest version
the catalog holds released for that `MAJOR.MINOR`, 0 for one it holds none of. The builder reserves it when the
module's build starts and passes it to the build of every artifact of the module (§370), so code and packed files
carry it; each artifact published is said as the event `package-published` of the builder's seat `mesh-builds`
(ADR 0666 §15), and the version is released only once every artifact of the module at it is published (§349); only the first watched branch releases (§371). A
failed publish leaves the version reserved, and `builder_publish_again` publishes the missing artifacts under the
same version. The seat `mesh-builds` is new, held once for the mesh, defined in `mesh-delivery-contracts`; the
builder held none until now, and its waits stay on its own stream in this milestone. Why: the operator, 2026-11-10
about 21:40 CET, a design choice of G's part (gaps items 22, 23 and 43). This reserves the number when the build
starts, where ADR 0666 §15 numbers a package at its publication and design 51 publishes a held digest under a new
number: a packed file carries its version, so it must be known before the build; the operator's choice, which a
later research brings the records in line with. How it is checked: test 102.
- §53 **Every dependent behind is rebuilt against it, on `package-published`.** The builder says `package-published`
for every package build of a watched branch: with its released version on the registration's first watched branch,
and on another watched branch by its digest alone, the build stored by digest with no version (§371). The runner,
which consumes it, asks the catalog's verb `outdated` for that package on that branch, a kept call (ADR 0668 §1,
§2), and starts a pipeline for each dependent by kind 1 whose recorded part digest differs from the published one:
the catalog compares (§375), the runner starts, and no other part rebuilds anything. A `package-published` whose
version is lower than the highest the catalog holds released for that module starts nothing (§382). Nothing else is
rebuilt, and a change to tests alone publishes nothing (ADR 0666 §15; research 086, R18). Why: ADR 0666 §15 has the
runner consume `package-published`; the digest comparison is the operator's (2026-11-10), and one event for both
branches is the design's choice, round 2 of the review by architectural domain, 2026-11-10.
- §54 **A second start writes nothing.** The runner starts a pipeline on two events, the indexer's `take-in.taken` (ADR
0666 §13) and `package-published` (ADR 0666 §15). When both reach it for the same source revision and module, the
second start writes nothing: the runner writes a pipeline's key (watched branch, source revision and module, *The
runner's kept data*) by compare-and-set on its absence, so one revision and module has one pipeline. The design's
choice under ADR 0666 §13 and §15: the key the runner already keeps is the one place a start is judged, so neither
event's producer needs to know of the other.
- §55 **Unread packages refuse.** A build that cannot read the published packages refuses and names the unread state
(ADR 0666 §15).
- §56 **How it is checked.** Whole (builder, indexer): a mesh-sdk merge changing one package publishes one version, and only
the artifacts importing that package rebuild; a merge changing only tests rebuilds nothing; a build whose packages
cannot be read refuses naming the state. Whole (runner): a `take-in.taken` and a `package-published` for the same
source revision and module start one pipeline, the second writing nothing. Tests 70, 80 and 101.

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