A manifest's requirement may be written {"name": "influxdb-api", "optional": true} (ADR 0330 §1). A bare name stays hard.
Resolution (internal/catalogue/resolve.go): a name only optional consumers want is settled after the walk. It binds exactly as a hard requirement when a provider is assigned or reachable (§2). Otherwise it is absent: not refused, and nothing is installed for it, not even the only catalogue candidate, on this machine or elsewhere (§3). A name another module wants hard (including one pulled in later) still resolves and installs as before. Resolution.Absent lists each absent requirement by consumer.
Composition (declaration.go, bound_into_files.go): ${bound:<provision>:<key>|default:<value>} renders its default while the requirement is absent (§4). A file marked only-when-bound: <requirement> is left out while it is absent (§5), and the field is dropped from every resource before sending. Binding, credential and grant files of an absent requirement are not composed (§6).
Module check (optional_requirements.go): refuses an optional ${bound:…} placeholder without a default outside an only-when-bound file, and a ${secret:…} of an optional requirement outside one. It also refuses only-when-bound on a hard or unrequired name or on a non-file, and a default on a hard requirement.
plan and node (cmd/mesh-controller/unbound.go, plan.go, nodes.go): each list unbound <requirement>, optional for <consumer>. For plan --json/--diff the line goes to stderr, which the verb answer carries, because the declaration envelope is parsed strictly by the node-engine (§7). modules marks a requirement (optional).
Why
Issue 166: soft dependencies became hard provisions, so grafana could not be assigned on a mesh without influxdb, and with influxdb in the catalogue it would have been installed for grafana.
parsing, round-trip and strictness of the long form;
absent with no provider in both passes, with one catalogue candidate, brokered and node-local, and nothing installed;
binds when a provider is offered;
one consumer's optional being another's hard, including a hard want met later;
a hard requirement still refused naming the candidate;
the module check's refusals and acceptances;
composition while absent (defaults rendered; only-when-bound, secret and binding files left out; field stripped) and while bound.
cmd/mesh-controller/unbound_test.go (store test, added to testdata/store-tests): with grafana assigned and influxdb only in the catalogue, plan laptop and node show laptop both say influxdb-api is unbound and influxdb is not installed. Once influxdb is assigned on anchor, grafana binds to it with no act on grafana.
MESH_TEST_POSTGRES=… sh merge-check.sh passes locally.
Design limits chosen
only-when-bound applies to files only (ADR 0330 §5 also says "contribution"). A contributes entry to an optional requirement is already composed only while it binds, because it goes to whoever answers it. No seat contribution in the catalogue needs the mark yet.
A default is never empty: |default:} is not a placeholder, and the sweep refuses it as a misspelling.
A placeholder whose requirement is bound but whose key the provider does not serve is still refused, default or not.
Rolling back below this controller while a registered manifest uses the long form would fail to read that manifest. Older controllers only expect bare names.
Fixes issue 166.
The catalogue PRs (mesh-catalog: grafana, home-assistant; mesh-media-catalog: ombi) depend on this one shipping first.
## What
A manifest's requirement may be written `{"name": "influxdb-api", "optional": true}` (ADR 0330 §1). A bare name stays hard.
- **Resolution** (`internal/catalogue/resolve.go`): a name only optional consumers want is settled after the walk. It binds exactly as a hard requirement when a provider is assigned or reachable (§2). Otherwise it is **absent**: not refused, and nothing is installed for it, not even the only catalogue candidate, on this machine or elsewhere (§3). A name another module wants hard (including one pulled in later) still resolves and installs as before. `Resolution.Absent` lists each absent requirement by consumer.
- **Composition** (`declaration.go`, `bound_into_files.go`): `${bound:<provision>:<key>|default:<value>}` renders its default while the requirement is absent (§4). A file marked `only-when-bound: <requirement>` is left out while it is absent (§5), and the field is dropped from every resource before sending. Binding, credential and grant files of an absent requirement are not composed (§6).
- **Module check** (`optional_requirements.go`): refuses an optional `${bound:…}` placeholder without a default outside an only-when-bound file, and a `${secret:…}` of an optional requirement outside one. It also refuses `only-when-bound` on a hard or unrequired name or on a non-file, and a default on a hard requirement.
- **plan and node** (`cmd/mesh-controller/unbound.go`, `plan.go`, `nodes.go`): each list `unbound <requirement>, optional for <consumer>`. For `plan --json`/`--diff` the line goes to stderr, which the verb answer carries, because the declaration envelope is parsed strictly by the node-engine (§7). `modules` marks a requirement `(optional)`.
## Why
Issue 166: soft dependencies became hard provisions, so grafana could not be assigned on a mesh without influxdb, and with influxdb in the catalogue it would have been installed for grafana.
## How it was tested
- Every test fails without the change.
- `internal/catalogue/optional_requirements_test.go` covers:
- parsing, round-trip and strictness of the long form;
- absent with no provider in both passes, with one catalogue candidate, brokered and node-local, and nothing installed;
- binds when a provider is offered;
- one consumer's optional being another's hard, including a hard want met later;
- a hard requirement still refused naming the candidate;
- the module check's refusals and acceptances;
- composition while absent (defaults rendered; only-when-bound, secret and binding files left out; field stripped) and while bound.
- `cmd/mesh-controller/unbound_test.go` (store test, added to `testdata/store-tests`): with grafana assigned and influxdb only in the catalogue, `plan laptop` and `node show laptop` both say `influxdb-api` is unbound and influxdb is not installed. Once influxdb is assigned on anchor, grafana binds to it with no act on grafana.
- `MESH_TEST_POSTGRES=… sh merge-check.sh` passes locally.
## Design limits chosen
- `only-when-bound` applies to files only (ADR 0330 §5 also says "contribution"). A `contributes` entry to an optional requirement is already composed only while it binds, because it goes to whoever answers it. No seat contribution in the catalogue needs the mark yet.
- A default is never empty: `|default:}` is not a placeholder, and the sweep refuses it as a misspelling.
- A placeholder whose requirement is bound but whose key the provider does not serve is still refused, default or not.
- Rolling back below this controller while a registered manifest uses the long form would fail to read that manifest. Older controllers only expect bare names.
Fixes issue 166.
The catalogue PRs (mesh-catalog: grafana, home-assistant; mesh-media-catalog: ombi) depend on this one shipping first.
Deliverynovox/mesh-controller@79a3396340a7 — delivered since 2026-10-13T00:15:31Z
Delivery plan — builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step
tier 0: mesh-controller
tier 1: build-agent
ace: receives build-agent
g14: receives build-agent
novox: receives mesh-controller, build-agent
shanks: receives build-agent
build-agent keeps data (ADR 0233): what it holds is backed up before it moves
grants step: if this changes what the bus's user list says (a new tool, subject or user), novox is sent that list alone, every build there kept, before build-agent is judged on its first machine
Machines
mesh-controller on novox: passed — healthy 3 times over 4m9s
mesh-controller on the rest running it: sent
build-agent on ace: passed — healthy 1 times over 16s
build-agent on the rest running it: sent
Transitions
2026-10-12 23:54 (new) → proposed (announced): novox/mesh-controller#281's head announced
2026-10-13 00:01 proposed → checked (checked): a verdict names this commit and the trunk's head as its trunk commit
2026-10-13 00:01 checked → ready (accepted): the gate passed or warned, and the repository's own check did not fail
2026-10-13 00:05 ready → published (merged): on the trunk its modules follow: merged there, and its walk opened — or nothing for a walk to move
2026-10-13 00:05 published → delivering (go): its walk started: let go by this owner in its group's order, by a person, or on the controller's own path — not a failed walk that carried a later commit for it, after which it waits for a walk of its own commit
2026-10-13 00:15 delivering → delivered (done): its walk is done, carrying its own commit: every machine of its deploy plan passed, or was left as its policy says
The commit's note under refs/notes/mesh-plan keeps every transition: git log --notes=mesh-plan.
<!-- mesh-delivery:view -->
**Delivery** `novox/mesh-controller@79a3396340a7` — **delivered** since 2026-10-13T00:15:31Z
**Delivery plan** — builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step
- tier 0: mesh-controller
- tier 1: build-agent
- ace: receives build-agent
- g14: receives build-agent
- novox: receives mesh-controller, build-agent
- shanks: receives build-agent
- build-agent keeps data (ADR 0233): what it holds is backed up before it moves
- grants step: if this changes what the bus's user list says (a new tool, subject or user), novox is sent that list alone, every build there kept, before build-agent is judged on its first machine
**Machines**
- mesh-controller on novox: passed — healthy 3 times over 4m9s
- mesh-controller on the rest running it: sent
- build-agent on ace: passed — healthy 1 times over 16s
- build-agent on the rest running it: sent
**Transitions**
- 2026-10-12 23:54 (new) → proposed (announced): novox/mesh-controller#281's head announced
- 2026-10-13 00:01 proposed → checked (checked): a verdict names this commit and the trunk's head as its trunk commit
- 2026-10-13 00:01 checked → ready (accepted): the gate passed or warned, and the repository's own check did not fail
- 2026-10-13 00:05 ready → published (merged): on the trunk its modules follow: merged there, and its walk opened — or nothing for a walk to move
- 2026-10-13 00:05 published → delivering (go): its walk started: let go by this owner in its group's order, by a person, or on the controller's own path — not a failed walk that carried a later commit for it, after which it waits for a walk of its own commit
- 2026-10-13 00:15 delivering → delivered (done): its walk is done, carrying its own commit: every machine of its deploy plan passed, or was left as its policy says
The commit's note under `refs/notes/mesh-plan` keeps every transition: `git log --notes=mesh-plan`.
Merge check at bc81029a
Judged: head bc81029a030a merged into trunk 56d6313262e8; read beside it: mesh-catalog 8f80cd471634, mesh-controller-main 56d6313262e8, mesh-host 7a41e120635b, mesh-tools 954e1a53eef9
mesh/merge-gate: PASS — every machine composes with the change as it did without (4 of 4 compose) (modules: build-agent, mesh-controller)
mesh/repo-check: FAIL — its merge-check.sh failed: --- FAIL: TestTheInstallersFirstUserListIsWhatTheControllerWouldCompose (0.00s)
Delivery plan — builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step
tier 0: mesh-controller
tier 1: build-agent
ace: receives build-agent
g14: receives build-agent
novox: receives mesh-controller, build-agent
shanks: receives build-agent
build-agent keeps data (ADR 0233): what it holds is backed up before it moves
grants step: if this changes what the bus's user list says (a new tool, subject or user), novox is sent that list alone, every build there kept, before build-agent is judged on its first machine
read by no module's build: cmd/mesh-controller/unbound_test.go, internal/catalogue/optional_requirements_test.go, testdata/store-tests
Run by the build seat on shanks as build-1791844826150324029 (builds --log build-1791844826150324029).
--- the gate: build-agent, mesh-controller
mesh-controller: ok, 58 tool(s); WARNING: merge-window ask(s) for a setting and do(es) not say whether it is trusted, so it counts as trusted: set at the terminal alone; say "trusted" false where nothing trusts it (novox/hq issue 339); WARNING: controller stay(s) up and say(s) not how it is ready — judged by liveness alone, refused from 2026-11-18 (ADR 0240 rule 8)
long-running resources without health: 1
files asking for a setting without saying whether it is trusted: 1
1 manifest(s) checked. Judged against the seats this binary carries; a claim on one of the mesh's own seats is judged fully at registration, and a seat declared by a module not given here reads as unknown. Identities judged on a 6-character machine name, against the bounds of the providers given here
judging 1 module(s) changed against 4 machine(s), as the snapshot of 2026-10-12T22:33:48Z says they run
composing every machine as the mesh is
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
composing every machine with the change
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
merge gate: PASS — every machine composes with the change as it did without (4 of 4 compose)
judged by controller development build against the facts of 2026-10-12T22:33:48Z
modules the change moves:
- mesh-controller (changed)
machines:
- a machine (f35): composes
- holds mesh-dns-resolver (idt): composes
- holds git, holds mesh-artifact-store, holds mesh-broker, holds mesh-catalog, holds mesh-delivery, holds mesh-dns-resolver, holds mesh-store, holds mesh-vault, holds npm-package-registry, the control node, the hub (ygcyr): composes
- a machine (yvbpsm): composes
a merge would rebuild 2 module(s) in 2 tier(s): build-agent, mesh-controller
notes:
- cmd/mesh-controller/unbound_test.go, internal/catalogue/optional_requirements_test.go and testdata/store-tests read by no module's build, and rebuild nothing (issue 280)
--- its merge-check.sh (go toolchain)
ok github.com/novox/mesh-controller/cmd/mesh-builder 1.052s
ok github.com/novox/mesh-controller/cmd/mesh-controller 190.857s
ok github.com/novox/mesh-controller/examples/objectstore-provisioner 1.051s
ok github.com/novox/mesh-controller/examples/postgres-provisioner 1.077s
ok github.com/novox/mesh-controller/examples/redis-provisioner 1.059s
ok github.com/novox/mesh-controller/examples/route-proxy 1.278s
ok github.com/novox/mesh-controller/internal/artifacts 1.115s
? github.com/novox/mesh-controller/internal/beside [no test files]
--- FAIL: TestTheInstallersFirstUserListIsWhatTheControllerWouldCompose (0.00s)
genesis_template_test.go:29: judged against /var/lib/build-agent/workspace/check/mesh-host as cloned beside this check
genesis_template_test.go:29: open /var/lib/build-agent/workspace/check/mesh-host/examples/foundation-first-node-nats.lock: no such file or directory
FAIL
FAIL github.com/novox/mesh-controller/internal/broker 2.848s
ok github.com/novox/mesh-controller/internal/builder 55.128s
ok github.com/novox/mesh-controller/internal/catalogue 9.195s
ok github.com/novox/mesh-controller/internal/conditions 2.729s
ok github.com/novox/mesh-controller/internal/envfile 1.065s
ok github.com/novox/mesh-controller/internal/facts 1.074s
ok github.com/novox/mesh-controller/internal/identity 5.384s
ok github.com/novox/mesh-controller/internal/inventory 51.195s
ok github.com/novox/mesh-controller/internal/lease 17.781s
ok github.com/novox/mesh-controller/internal/licences 4.266s
ok github.com/novox/mesh-controller/internal/licences/adapters 1.061s
ok github.com/novox/mesh-controller/internal/link 60.238s
ok github.com/novox/mesh-controller/internal/lint 2.519s
ok github.com/novox/mesh-controller/internal/outward 1.110s
ok github.com/novox/mesh-controller/internal/overlay 1.080s
ok github.com/novox/mesh-controller/internal/secrets 1.558s
ok github.com/novox/mesh-controller/internal/store 3.574s
ok github.com/novox/mesh-controller/internal/testbus 1.280s
ok github.com/novox/mesh-controller/internal/token 1.018s
FAIL
--- what failed, picked from the whole of its output (the whole is in the build's log)
--- FAIL: TestTheInstallersFirstUserListIsWhatTheControllerWouldCompose (0.00s)
genesis_template_test.go:29: judged against /var/lib/build-agent/workspace/check/mesh-host as cloned beside this check
genesis_template_test.go:29: open /var/lib/build-agent/workspace/check/mesh-host/examples/foundation-first-node-nats.lock: no such file or directory
FAIL github.com/novox/mesh-controller/internal/broker 2.848s
**Merge check** at `bc81029a`
Judged: head `bc81029a030a` merged into trunk `56d6313262e8`; read beside it: mesh-catalog `8f80cd471634`, mesh-controller-main `56d6313262e8`, mesh-host `7a41e120635b`, mesh-tools `954e1a53eef9`
- `mesh/merge-gate`: **PASS** — every machine composes with the change as it did without (4 of 4 compose) (modules: build-agent, mesh-controller)
- `mesh/repo-check`: **FAIL** — its merge-check.sh failed: --- FAIL: TestTheInstallersFirstUserListIsWhatTheControllerWouldCompose (0.00s)
**Delivery plan** — builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step
- tier 0: mesh-controller
- tier 1: build-agent
- ace: receives build-agent
- g14: receives build-agent
- novox: receives mesh-controller, build-agent
- shanks: receives build-agent
- build-agent keeps data (ADR 0233): what it holds is backed up before it moves
- grants step: if this changes what the bus's user list says (a new tool, subject or user), novox is sent that list alone, every build there kept, before build-agent is judged on its first machine
- read by no module's build: cmd/mesh-controller/unbound_test.go, internal/catalogue/optional_requirements_test.go, testdata/store-tests
Run by the build seat on shanks as `build-1791844826150324029` (`builds --log build-1791844826150324029`).
```
--- the gate: build-agent, mesh-controller
mesh-controller: ok, 58 tool(s); WARNING: merge-window ask(s) for a setting and do(es) not say whether it is trusted, so it counts as trusted: set at the terminal alone; say "trusted" false where nothing trusts it (novox/hq issue 339); WARNING: controller stay(s) up and say(s) not how it is ready — judged by liveness alone, refused from 2026-11-18 (ADR 0240 rule 8)
long-running resources without health: 1
files asking for a setting without saying whether it is trusted: 1
1 manifest(s) checked. Judged against the seats this binary carries; a claim on one of the mesh's own seats is judged fully at registration, and a seat declared by a module not given here reads as unknown. Identities judged on a 6-character machine name, against the bounds of the providers given here
judging 1 module(s) changed against 4 machine(s), as the snapshot of 2026-10-12T22:33:48Z says they run
composing every machine as the mesh is
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
composing every machine with the change
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
merge gate: PASS — every machine composes with the change as it did without (4 of 4 compose)
judged by controller development build against the facts of 2026-10-12T22:33:48Z
modules the change moves:
- mesh-controller (changed)
machines:
- a machine (f35): composes
- holds mesh-dns-resolver (idt): composes
- holds git, holds mesh-artifact-store, holds mesh-broker, holds mesh-catalog, holds mesh-delivery, holds mesh-dns-resolver, holds mesh-store, holds mesh-vault, holds npm-package-registry, the control node, the hub (ygcyr): composes
- a machine (yvbpsm): composes
a merge would rebuild 2 module(s) in 2 tier(s): build-agent, mesh-controller
notes:
- cmd/mesh-controller/unbound_test.go, internal/catalogue/optional_requirements_test.go and testdata/store-tests read by no module's build, and rebuild nothing (issue 280)
--- its merge-check.sh (go toolchain)
ok github.com/novox/mesh-controller/cmd/mesh-builder 1.052s
ok github.com/novox/mesh-controller/cmd/mesh-controller 190.857s
ok github.com/novox/mesh-controller/examples/objectstore-provisioner 1.051s
ok github.com/novox/mesh-controller/examples/postgres-provisioner 1.077s
ok github.com/novox/mesh-controller/examples/redis-provisioner 1.059s
ok github.com/novox/mesh-controller/examples/route-proxy 1.278s
ok github.com/novox/mesh-controller/internal/artifacts 1.115s
? github.com/novox/mesh-controller/internal/beside [no test files]
--- FAIL: TestTheInstallersFirstUserListIsWhatTheControllerWouldCompose (0.00s)
genesis_template_test.go:29: judged against /var/lib/build-agent/workspace/check/mesh-host as cloned beside this check
genesis_template_test.go:29: open /var/lib/build-agent/workspace/check/mesh-host/examples/foundation-first-node-nats.lock: no such file or directory
FAIL
FAIL github.com/novox/mesh-controller/internal/broker 2.848s
ok github.com/novox/mesh-controller/internal/builder 55.128s
ok github.com/novox/mesh-controller/internal/catalogue 9.195s
ok github.com/novox/mesh-controller/internal/conditions 2.729s
ok github.com/novox/mesh-controller/internal/envfile 1.065s
ok github.com/novox/mesh-controller/internal/facts 1.074s
ok github.com/novox/mesh-controller/internal/identity 5.384s
ok github.com/novox/mesh-controller/internal/inventory 51.195s
ok github.com/novox/mesh-controller/internal/lease 17.781s
ok github.com/novox/mesh-controller/internal/licences 4.266s
ok github.com/novox/mesh-controller/internal/licences/adapters 1.061s
ok github.com/novox/mesh-controller/internal/link 60.238s
ok github.com/novox/mesh-controller/internal/lint 2.519s
ok github.com/novox/mesh-controller/internal/outward 1.110s
ok github.com/novox/mesh-controller/internal/overlay 1.080s
ok github.com/novox/mesh-controller/internal/secrets 1.558s
ok github.com/novox/mesh-controller/internal/store 3.574s
ok github.com/novox/mesh-controller/internal/testbus 1.280s
ok github.com/novox/mesh-controller/internal/token 1.018s
FAIL
--- what failed, picked from the whole of its output (the whole is in the build's log)
--- FAIL: TestTheInstallersFirstUserListIsWhatTheControllerWouldCompose (0.00s)
genesis_template_test.go:29: judged against /var/lib/build-agent/workspace/check/mesh-host as cloned beside this check
genesis_template_test.go:29: open /var/lib/build-agent/workspace/check/mesh-host/examples/foundation-first-node-nats.lock: no such file or directory
FAIL github.com/novox/mesh-controller/internal/broker 2.848s
```
Independent review for hq issue 166 (ADR 0330). I am asking for one change: the form a registered manifest is stored in (finding 4). Everything else I checked holds, and the evidence is below.
1. Today's mesh composes byte-identically. Proven. I merged this head into main 56d6313 locally; it merges cleanly. I took the facts snapshot of 2026-10-12 22:43:48Z and composed every machine twice with main and once with the merge. Each run used the gate's own path (raiseFromFacts, then composedAndValidated, then sendable.Body()) against a throwaway Postgres. Both controllers produced the same resource ids in the same order on all 4 machines: 385, 718, 540 and 361 resources. The only bytes that differ are those that also differ between two runs of main: sealed values, generated secrets, and 5 file contents whose only difference is a random token. With those masked, main and the merge are identical. The node-engine's validator refuses nothing in either.
2. Resolution works as decided. The tests cover each case, and I mutated the code to show each test catches its case. Optional with no provider: absent, nothing installed, on both passes. Optional with a provider elsewhere: binds as a hard requirement would. One consumer's optional want and another's hard want: still refused, or installed when a module pulled in later wants it hard. On real data, I made a copy of the snapshot with influxdb, lidarr, mosquitto and nodered unassigned on ace, and laid catalogue #259 and media #36 over it. ace composes and validates, and plan lists four unbound lines. With nodered left assigned, mqtt-topic is refused because nodered wants it hard. That is correct.
3. only-when-bound is stripped before sending.withoutAbsent drops the field from every resource it keeps. In every composed body (the snapshot as it is, with #259 and #36, and the absent copy) no resource carries only-when-bound.
4. Rollback to an older controller: not acceptable as written. Keep a plain-name copy.Inventory.Catalogue() unmarshals every stored manifest and returns an error on the first one it cannot read. I checked the controller at main against both forms:
the long form fails with cannot unmarshal object into manifestFields.requires.1 of type string;
{"requires":["a","b"],"optional-requires":["b"]} reads cleanly, because the extra key goes to unknown.
So once #259 or #36 is registered, a controller older than this one cannot resolve any machine. Every plan and every push fails, and the controller cannot heal itself. A merge gate catches a revert, but a person's push or a rebuild of an older controller build does not go through one, and on 2026-10-02 an older controller build did reach the control node.
The fix is small and stays inside the controller: have MarshalJSON write requires as plain names plus an optional-requires list, and have UnmarshalJSON read that as well as the long form. The catalogue keeps the long form in its source files. An older controller would then treat the optional requirements as hard, which composes today, because each one is bound on ace. At worst it refuses or installs for that one module, as it did before ADR 0330. That is far less than every machine failing to compose. Please add a test that stores a manifest with an optional requirement and reads it back through the old field shape.
5. Grafana's data source on ace is covered on #259: it keeps its uid, and the files move with the old paths removed as undeclared.
6. The tests fail without the changes. I took out each part of the change in turn and ran the tests:
absent and settling turned off: 3 tests fail;
the wait for the rest of the walk turned off: TestOneConsumersOptionalRequirementIsAnothersHardOne fails;
withoutAbsent turned off: 2 tests fail;
OptionalProblems taken out of the module check: the module-check test fails;
the default ignored: the defaults test fails;
unboundLines emptied: TestPlanAndNodeListAnOptionalRequirementLeftUnbound fails, for both plan and node.
Checks. I read every status on this head. mesh/merge-gate passed at 22:46 against trunk 56d6313 ("builds build-agent, mesh-controller → ace, g14, novox, shanks"). It has been marked out of date since trunk moved to cbac703. mesh/repo-check failed at 22:46 on TestTheInstallersFirstUserListIsWhatTheControllerWouldCompose, and this change did not cause it. mesh-host 37947ca (hq issue 146, ADR 0331) removed examples/foundation-first-node-nats.lock, but internal/broker/genesis_template_test.go on controller main (cbac703 too) still reads that file. Against mesh-host main as the clone beside the check, main fails the same way. Every controller pull request will be red on this until that test follows mesh-host. Locally, without that clone (the captured copy in testdata/beside), merge-check.sh against a throwaway Postgres 18 passes: gofmt, go vet, and go test -race over the whole suite, with all 37 listed store tests passing, including the new one. The first full run failed once on internal/lease TestKeepingRenewsAcrossManyAges ("context deadline exceeded" under load). That package passed 3 of 3 runs on its own, and the second full run passed. It is a flake unrelated to this change.
Not blocking:
The refusal names the wrong consumer. It reads "wanted by %s" with because[want], which is the first module that queued the name. On the absent copy it said: nothing in this mesh provides "mqtt-topic", wanted by home-assistant. home-assistant wants it only optionally, and nodered is the module that wants it hard. Name a hard wanter when there is one.
A restart list can point at an absent id. A manifest's restart-on can name an only-when-bound file, or the bound and secret ids of an absent requirement. The node-engine accepts this and reads an empty digest, so binding later restarts the container, which is the behaviour we want. A line in the module-check comment would stop the next reader wondering.
Contributions. ADR 0330 §5 says "a file or contribution" may be only-when-bound. The check refuses the field on anything but a file. A contribution to an absent requirement has no provider to receive it, so the result is the same, but the comment in OptionalProblems should say so.
Verdict: request changes at bc81029a030a4bdd87a6cb05c40ed3dc679cd93d
Independent review for hq issue 166 (ADR 0330). I am asking for one change: the form a registered manifest is stored in (finding 4). Everything else I checked holds, and the evidence is below.
**1. Today's mesh composes byte-identically. Proven.** I merged this head into main 56d6313 locally; it merges cleanly. I took the facts snapshot of 2026-10-12 22:43:48Z and composed every machine twice with main and once with the merge. Each run used the gate's own path (raiseFromFacts, then composedAndValidated, then `sendable.Body()`) against a throwaway Postgres. Both controllers produced the same resource ids in the same order on all 4 machines: 385, 718, 540 and 361 resources. The only bytes that differ are those that also differ between two runs of main: sealed values, generated secrets, and 5 file contents whose only difference is a random token. With those masked, main and the merge are identical. The node-engine's validator refuses nothing in either.
**2. Resolution works as decided.** The tests cover each case, and I mutated the code to show each test catches its case. Optional with no provider: absent, nothing installed, on both passes. Optional with a provider elsewhere: binds as a hard requirement would. One consumer's optional want and another's hard want: still refused, or installed when a module pulled in later wants it hard. On real data, I made a copy of the snapshot with influxdb, lidarr, mosquitto and nodered unassigned on ace, and laid catalogue #259 and media #36 over it. ace composes and validates, and `plan` lists four unbound lines. With nodered left assigned, mqtt-topic is refused because nodered wants it hard. That is correct.
**3. `only-when-bound` is stripped before sending.** `withoutAbsent` drops the field from every resource it keeps. In every composed body (the snapshot as it is, with #259 and #36, and the absent copy) no resource carries `only-when-bound`.
**4. Rollback to an older controller: not acceptable as written. Keep a plain-name copy.** `Inventory.Catalogue()` unmarshals every stored manifest and returns an error on the first one it cannot read. I checked the controller at main against both forms:
- the long form fails with `cannot unmarshal object into manifestFields.requires.1 of type string`;
- `{"requires":["a","b"],"optional-requires":["b"]}` reads cleanly, because the extra key goes to `unknown`.
So once #259 or #36 is registered, a controller older than this one cannot resolve any machine. Every plan and every push fails, and the controller cannot heal itself. A merge gate catches a revert, but a person's `push` or a rebuild of an older controller build does not go through one, and on 2026-10-02 an older controller build did reach the control node.
The fix is small and stays inside the controller: have `MarshalJSON` write `requires` as plain names plus an `optional-requires` list, and have `UnmarshalJSON` read that as well as the long form. The catalogue keeps the long form in its source files. An older controller would then treat the optional requirements as hard, which composes today, because each one is bound on ace. At worst it refuses or installs for that one module, as it did before ADR 0330. That is far less than every machine failing to compose. Please add a test that stores a manifest with an optional requirement and reads it back through the old field shape.
**5. Grafana's data source on ace** is covered on #259: it keeps its uid, and the files move with the old paths removed as undeclared.
**6. The tests fail without the changes.** I took out each part of the change in turn and ran the tests:
- absent and settling turned off: 3 tests fail;
- the wait for the rest of the walk turned off: TestOneConsumersOptionalRequirementIsAnothersHardOne fails;
- `withoutAbsent` turned off: 2 tests fail;
- `OptionalProblems` taken out of the module check: the module-check test fails;
- the default ignored: the defaults test fails;
- `unboundLines` emptied: TestPlanAndNodeListAnOptionalRequirementLeftUnbound fails, for both plan and node.
**Checks.** I read every status on this head. `mesh/merge-gate` passed at 22:46 against trunk 56d6313 ("builds build-agent, mesh-controller → ace, g14, novox, shanks"). It has been marked out of date since trunk moved to cbac703. `mesh/repo-check` failed at 22:46 on `TestTheInstallersFirstUserListIsWhatTheControllerWouldCompose`, and this change did not cause it. mesh-host 37947ca (hq issue 146, ADR 0331) removed `examples/foundation-first-node-nats.lock`, but `internal/broker/genesis_template_test.go` on controller main (cbac703 too) still reads that file. Against mesh-host main as the clone beside the check, main fails the same way. Every controller pull request will be red on this until that test follows mesh-host. Locally, without that clone (the captured copy in testdata/beside), merge-check.sh against a throwaway Postgres 18 passes: gofmt, go vet, and `go test -race` over the whole suite, with all 37 listed store tests passing, including the new one. The first full run failed once on `internal/lease` TestKeepingRenewsAcrossManyAges ("context deadline exceeded" under load). That package passed 3 of 3 runs on its own, and the second full run passed. It is a flake unrelated to this change.
Not blocking:
- **The refusal names the wrong consumer.** It reads "wanted by %s" with `because[want]`, which is the first module that queued the name. On the absent copy it said: nothing in this mesh provides "mqtt-topic", wanted by home-assistant. home-assistant wants it only optionally, and nodered is the module that wants it hard. Name a hard wanter when there is one.
- **A restart list can point at an absent id.** A manifest's restart-on can name an `only-when-bound` file, or the bound and secret ids of an absent requirement. The node-engine accepts this and reads an empty digest, so binding later restarts the container, which is the behaviour we want. A line in the module-check comment would stop the next reader wondering.
- **Contributions.** ADR 0330 §5 says "a file or contribution" may be `only-when-bound`. The check refuses the field on anything but a file. A contribution to an absent requirement has no provider to receive it, so the result is the same, but the comment in OptionalProblems should say so.
The stored form reads on an older controller.MarshalJSON now writes requires as a plain list of names, with the optional ones in a separate key, optional-requires. UnmarshalJSON reads both shapes: the {name, optional} entry and the separate list. The module check refuses an optional-requires entry the module does not require.
Test:TestTheStoredFormReadsWhereRequiresIsAListOfNames unmarshals the stored form into main's struct{ Requires []string }. It also reads it back here, both leniently and through ParseManifest.
Checked against main's code: I unmarshalled the stored grafana manifest with a controller built from origin/main into catalogue.Manifest. The error is nil and requires is [route influxdb-api]. The unknown optional-requires key is set aside, so only that module is left out and the catalogue still reads.
Catalogue files (mesh-catalog #259, mesh-media-catalog #36) unchanged. Every lenient read of a manifest in the controller reads the store (Catalogue, Catalogued, snapshots for the gate, upgradeFrom), and the store always holds this controller's MarshalJSON form. A catalogue file is only read through the strict ParseManifest. On an older controller that refuses the file whichever form it uses (a type error or an unknown key), and only that one registration. So the files keep ADR 0330's {name, optional} form.
Optional review item, refusal naming: a hard requirement refused when another module met the name first as optional now names a module that wants it hard (wanted by telegraf, tested). Before, it named the optional consumer.
Optional review item, only-when-bound on contributions: not built. A contributes entry is keyed by the requirement and goes only to whoever answers it, so it is already composed only while the requirement is bound. A seat contribution is not tied to a requirement, and none in the catalogues needs it. I can add it when one does.
Local check:MESH_TEST_POSTGRES=… sh merge-check.sh exit 0, including the store test TestPlanAndNodeListAnOptionalRequirementLeftUnbound. I judged it locally because repo-check is red on main for the unrelated genesis bundle.
**Review 112 addressed at `f0a62c1`.**
- **The stored form reads on an older controller.** `MarshalJSON` now writes `requires` as a plain list of names, with the optional ones in a separate key, `optional-requires`. `UnmarshalJSON` reads both shapes: the `{name, optional}` entry and the separate list. The module check refuses an `optional-requires` entry the module does not require.
- **Test:** `TestTheStoredFormReadsWhereRequiresIsAListOfNames` unmarshals the stored form into main's `struct{ Requires []string }`. It also reads it back here, both leniently and through `ParseManifest`.
- **Checked against main's code:** I unmarshalled the stored grafana manifest with a controller built from origin/main into `catalogue.Manifest`. The error is nil and requires is `[route influxdb-api]`. The unknown `optional-requires` key is set aside, so only that module is left out and the catalogue still reads.
- **Catalogue files (mesh-catalog #259, mesh-media-catalog #36) unchanged.** Every lenient read of a manifest in the controller reads the store (`Catalogue`, `Catalogued`, snapshots for the gate, `upgradeFrom`), and the store always holds this controller's `MarshalJSON` form. A catalogue file is only read through the strict `ParseManifest`. On an older controller that refuses the file whichever form it uses (a type error or an unknown key), and only that one registration. So the files keep ADR 0330's `{name, optional}` form.
- **Optional review item, refusal naming:** a hard requirement refused when another module met the name first as optional now names a module that wants it hard (`wanted by telegraf`, tested). Before, it named the optional consumer.
- **Optional review item, `only-when-bound` on contributions:** not built. A `contributes` entry is keyed by the requirement and goes only to whoever answers it, so it is already composed only while the requirement is bound. A seat contribution is not tied to a requirement, and none in the catalogues needs it. I can add it when one does.
- **Local check:** `MESH_TEST_POSTGRES=… sh merge-check.sh` exit 0, including the store test `TestPlanAndNodeListAnOptionalRequirementLeftUnbound`. I judged it locally because repo-check is red on main for the unrelated genesis bundle.
Merge check at f0a62c1b
Judged: head f0a62c1b473f merged into trunk cbac703e817d; read beside it: mesh-catalog 8f80cd471634, mesh-controller-main cbac703e817d, mesh-host 7a41e120635b, mesh-tools 954e1a53eef9
mesh/merge-gate: PASS — every machine composes with the change as it did without (4 of 4 compose) (modules: build-agent, mesh-controller)
mesh/repo-check: FAIL — its merge-check.sh failed: --- FAIL: TestTheInstallersFirstUserListIsWhatTheControllerWouldCompose (0.00s)
Delivery plan — builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step
tier 0: mesh-controller
tier 1: build-agent
ace: receives build-agent
g14: receives build-agent
novox: receives mesh-controller, build-agent
shanks: receives build-agent
build-agent keeps data (ADR 0233): what it holds is backed up before it moves
grants step: if this changes what the bus's user list says (a new tool, subject or user), novox is sent that list alone, every build there kept, before build-agent is judged on its first machine
read by no module's build: cmd/mesh-controller/unbound_test.go, internal/catalogue/optional_requirements_test.go, testdata/store-tests
Run by the build seat on shanks as build-1791847135848669029 (builds --log build-1791847135848669029).
--- the gate: build-agent, mesh-controller
mesh-controller: ok, 58 tool(s); WARNING: merge-window ask(s) for a setting and do(es) not say whether it is trusted, so it counts as trusted: set at the terminal alone; say "trusted" false where nothing trusts it (novox/hq issue 339); WARNING: controller stay(s) up and say(s) not how it is ready — judged by liveness alone, refused from 2026-11-18 (ADR 0240 rule 8)
long-running resources without health: 1
files asking for a setting without saying whether it is trusted: 1
1 manifest(s) checked. Judged against the seats this binary carries; a claim on one of the mesh's own seats is judged fully at registration, and a seat declared by a module not given here reads as unknown. Identities judged on a 6-character machine name, against the bounds of the providers given here
judging 1 module(s) changed against 4 machine(s), as the snapshot of 2026-10-12T23:09:52Z says they run
composing every machine as the mesh is
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
composing every machine with the change
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
merge gate: PASS — every machine composes with the change as it did without (4 of 4 compose)
judged by controller development build against the facts of 2026-10-12T23:09:52Z
modules the change moves:
- mesh-controller (changed)
machines:
- a machine (f35): composes
- holds mesh-dns-resolver (idt): composes
- holds git, holds mesh-artifact-store, holds mesh-broker, holds mesh-catalog, holds mesh-delivery, holds mesh-dns-resolver, holds mesh-store, holds mesh-vault, holds npm-package-registry, the control node, the hub (ygcyr): composes
- a machine (yvbpsm): composes
a merge would rebuild 2 module(s) in 2 tier(s): build-agent, mesh-controller
notes:
- cmd/mesh-controller/unbound_test.go, internal/catalogue/optional_requirements_test.go and testdata/store-tests read by no module's build, and rebuild nothing (issue 280)
--- its merge-check.sh (go toolchain)
ok github.com/novox/mesh-controller/cmd/mesh-builder 1.042s
ok github.com/novox/mesh-controller/cmd/mesh-controller 174.192s
ok github.com/novox/mesh-controller/examples/objectstore-provisioner 1.024s
ok github.com/novox/mesh-controller/examples/postgres-provisioner 1.079s
ok github.com/novox/mesh-controller/examples/redis-provisioner 1.044s
ok github.com/novox/mesh-controller/examples/route-proxy 1.235s
ok github.com/novox/mesh-controller/internal/artifacts 1.090s
? github.com/novox/mesh-controller/internal/beside [no test files]
--- FAIL: TestTheInstallersFirstUserListIsWhatTheControllerWouldCompose (0.00s)
genesis_template_test.go:29: judged against /var/lib/build-agent/workspace/check/mesh-host as cloned beside this check
genesis_template_test.go:29: open /var/lib/build-agent/workspace/check/mesh-host/examples/foundation-first-node-nats.lock: no such file or directory
FAIL
FAIL github.com/novox/mesh-controller/internal/broker 2.826s
ok github.com/novox/mesh-controller/internal/builder 40.691s
ok github.com/novox/mesh-controller/internal/catalogue 8.908s
ok github.com/novox/mesh-controller/internal/conditions 2.727s
ok github.com/novox/mesh-controller/internal/envfile 1.069s
ok github.com/novox/mesh-controller/internal/facts 1.068s
ok github.com/novox/mesh-controller/internal/identity 5.118s
ok github.com/novox/mesh-controller/internal/inventory 35.773s
ok github.com/novox/mesh-controller/internal/lease 17.749s
ok github.com/novox/mesh-controller/internal/licences 4.324s
ok github.com/novox/mesh-controller/internal/licences/adapters 1.057s
ok github.com/novox/mesh-controller/internal/link 59.672s
ok github.com/novox/mesh-controller/internal/lint 2.545s
ok github.com/novox/mesh-controller/internal/outward 1.091s
ok github.com/novox/mesh-controller/internal/overlay 1.094s
ok github.com/novox/mesh-controller/internal/secrets 1.556s
ok github.com/novox/mesh-controller/internal/store 3.596s
ok github.com/novox/mesh-controller/internal/testbus 1.277s
ok github.com/novox/mesh-controller/internal/token 1.021s
FAIL
--- what failed, picked from the whole of its output (the whole is in the build's log)
--- FAIL: TestTheInstallersFirstUserListIsWhatTheControllerWouldCompose (0.00s)
genesis_template_test.go:29: judged against /var/lib/build-agent/workspace/check/mesh-host as cloned beside this check
genesis_template_test.go:29: open /var/lib/build-agent/workspace/check/mesh-host/examples/foundation-first-node-nats.lock: no such file or directory
FAIL github.com/novox/mesh-controller/internal/broker 2.826s
Out of date when it landed: out of date: judged against trunk cbac703e81, the trunk is now 7b2a9eae8020; judged again on a recheck or when it is merged
**Merge check** at `f0a62c1b`
Judged: head `f0a62c1b473f` merged into trunk `cbac703e817d`; read beside it: mesh-catalog `8f80cd471634`, mesh-controller-main `cbac703e817d`, mesh-host `7a41e120635b`, mesh-tools `954e1a53eef9`
- `mesh/merge-gate`: **PASS** — every machine composes with the change as it did without (4 of 4 compose) (modules: build-agent, mesh-controller)
- `mesh/repo-check`: **FAIL** — its merge-check.sh failed: --- FAIL: TestTheInstallersFirstUserListIsWhatTheControllerWouldCompose (0.00s)
**Delivery plan** — builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step
- tier 0: mesh-controller
- tier 1: build-agent
- ace: receives build-agent
- g14: receives build-agent
- novox: receives mesh-controller, build-agent
- shanks: receives build-agent
- build-agent keeps data (ADR 0233): what it holds is backed up before it moves
- grants step: if this changes what the bus's user list says (a new tool, subject or user), novox is sent that list alone, every build there kept, before build-agent is judged on its first machine
- read by no module's build: cmd/mesh-controller/unbound_test.go, internal/catalogue/optional_requirements_test.go, testdata/store-tests
Run by the build seat on shanks as `build-1791847135848669029` (`builds --log build-1791847135848669029`).
```
--- the gate: build-agent, mesh-controller
mesh-controller: ok, 58 tool(s); WARNING: merge-window ask(s) for a setting and do(es) not say whether it is trusted, so it counts as trusted: set at the terminal alone; say "trusted" false where nothing trusts it (novox/hq issue 339); WARNING: controller stay(s) up and say(s) not how it is ready — judged by liveness alone, refused from 2026-11-18 (ADR 0240 rule 8)
long-running resources without health: 1
files asking for a setting without saying whether it is trusted: 1
1 manifest(s) checked. Judged against the seats this binary carries; a claim on one of the mesh's own seats is judged fully at registration, and a seat declared by a module not given here reads as unknown. Identities judged on a 6-character machine name, against the bounds of the providers given here
judging 1 module(s) changed against 4 machine(s), as the snapshot of 2026-10-12T23:09:52Z says they run
composing every machine as the mesh is
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
composing every machine with the change
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
merge gate: PASS — every machine composes with the change as it did without (4 of 4 compose)
judged by controller development build against the facts of 2026-10-12T23:09:52Z
modules the change moves:
- mesh-controller (changed)
machines:
- a machine (f35): composes
- holds mesh-dns-resolver (idt): composes
- holds git, holds mesh-artifact-store, holds mesh-broker, holds mesh-catalog, holds mesh-delivery, holds mesh-dns-resolver, holds mesh-store, holds mesh-vault, holds npm-package-registry, the control node, the hub (ygcyr): composes
- a machine (yvbpsm): composes
a merge would rebuild 2 module(s) in 2 tier(s): build-agent, mesh-controller
notes:
- cmd/mesh-controller/unbound_test.go, internal/catalogue/optional_requirements_test.go and testdata/store-tests read by no module's build, and rebuild nothing (issue 280)
--- its merge-check.sh (go toolchain)
ok github.com/novox/mesh-controller/cmd/mesh-builder 1.042s
ok github.com/novox/mesh-controller/cmd/mesh-controller 174.192s
ok github.com/novox/mesh-controller/examples/objectstore-provisioner 1.024s
ok github.com/novox/mesh-controller/examples/postgres-provisioner 1.079s
ok github.com/novox/mesh-controller/examples/redis-provisioner 1.044s
ok github.com/novox/mesh-controller/examples/route-proxy 1.235s
ok github.com/novox/mesh-controller/internal/artifacts 1.090s
? github.com/novox/mesh-controller/internal/beside [no test files]
--- FAIL: TestTheInstallersFirstUserListIsWhatTheControllerWouldCompose (0.00s)
genesis_template_test.go:29: judged against /var/lib/build-agent/workspace/check/mesh-host as cloned beside this check
genesis_template_test.go:29: open /var/lib/build-agent/workspace/check/mesh-host/examples/foundation-first-node-nats.lock: no such file or directory
FAIL
FAIL github.com/novox/mesh-controller/internal/broker 2.826s
ok github.com/novox/mesh-controller/internal/builder 40.691s
ok github.com/novox/mesh-controller/internal/catalogue 8.908s
ok github.com/novox/mesh-controller/internal/conditions 2.727s
ok github.com/novox/mesh-controller/internal/envfile 1.069s
ok github.com/novox/mesh-controller/internal/facts 1.068s
ok github.com/novox/mesh-controller/internal/identity 5.118s
ok github.com/novox/mesh-controller/internal/inventory 35.773s
ok github.com/novox/mesh-controller/internal/lease 17.749s
ok github.com/novox/mesh-controller/internal/licences 4.324s
ok github.com/novox/mesh-controller/internal/licences/adapters 1.057s
ok github.com/novox/mesh-controller/internal/link 59.672s
ok github.com/novox/mesh-controller/internal/lint 2.545s
ok github.com/novox/mesh-controller/internal/outward 1.091s
ok github.com/novox/mesh-controller/internal/overlay 1.094s
ok github.com/novox/mesh-controller/internal/secrets 1.556s
ok github.com/novox/mesh-controller/internal/store 3.596s
ok github.com/novox/mesh-controller/internal/testbus 1.277s
ok github.com/novox/mesh-controller/internal/token 1.021s
FAIL
--- what failed, picked from the whole of its output (the whole is in the build's log)
--- FAIL: TestTheInstallersFirstUserListIsWhatTheControllerWouldCompose (0.00s)
genesis_template_test.go:29: judged against /var/lib/build-agent/workspace/check/mesh-host as cloned beside this check
genesis_template_test.go:29: open /var/lib/build-agent/workspace/check/mesh-host/examples/foundation-first-node-nats.lock: no such file or directory
FAIL github.com/novox/mesh-controller/internal/broker 2.826s
```
**Out of date when it landed**: out of date: judged against trunk cbac703e817d, the trunk is now 7b2a9eae8020; judged again on a recheck or when it is merged
Re-review against my review 112, for hq issue 166 (ADR 0330). The rollback finding is resolved, and nothing new came up.
The stored form now reads on an older controller. I proved it end to end. This head strict-parsed the files of #259 (grafana, home-assistant) and #36 (ombi), long form included, and marshalled them as registration stores them. The result is a plain requires list plus optional-requires (grafana: ["route","oidc-client","influxdb-api"] and ["influxdb-api"]). I then read those bytes with the controller at main (cbac703) through its own Manifest.UnmarshalJSON, the decoder Inventory.Catalogue() uses. All three read without error, with the full requires, and UnknownField() names optional-requires. After a rollback, those three modules are therefore left out of every machine by name under ADR 0262: nothing of them changes on their machines, and a condition is raised. The rest of the catalogue resolves. That is the rollback behaviour the mesh already designed for, and much better than every plan failing. (It is not "read as hard", as I suggested in 112. Leaving the module out is the safer of the two.)
The builder's claim holds: no path in main reads a catalogue file leniently. At main, every place that reads a module.json from a tree uses the strict catalogue.ParseManifest: the builder (internal/builder/builder.go:224), module check (check.go, modules.go), the merge gate's shelfWithChange, and build results (build.go:185, 614, 692, 783). After a rollback, those paths refuse the long form loudly, for that module alone. The lenient json.Unmarshal reads are all of stored bytes: Inventory.Catalogue, upgradeFrom and the bus-provider check in inventory/catalogue.go, and the facts snapshot's shelfOfFacts and graphOfFacts. With this head, every stored byte is written by MarshalJSON in the plain form. withReferences reads into a generic map and hands back the bytes unchanged, and link.ModuleOf reads only module. So keeping the long form in #259 and #36's files is fine.
Today's mesh still composes byte-identically. I merged this head into main cbac703 locally; it merges cleanly. I took a fresh facts snapshot (23:09:52Z), composed all 4 machines twice with main and once with the merge, and compared the wire bodies. The resource ids and their order are the same (385, 718, 540 and 361 resources). With the values that differ between two runs of main masked (sealed values, generated secrets, and 5 contents that differ only in a random token), there are 0 differences. The node-engine's validator refuses nothing.
The refusal now names a hard wanter. On my copy of the snapshot with influxdb, lidarr and mosquitto unassigned on ace (#259 and #36 laid over it), it reads: nothing in this mesh provides "mqtt-topic", wanted by nodered. It used to name home-assistant. With nodered unassigned as well, ace composes and validates, with the same 4 unbound lines as before. The rename only fires when the first wanter wanted the name optionally, because because[r] is set from Wants(), so a mesh without optional requirements is untouched. The byte-identity result confirms this.
The new tests fail without the change. Each time I removed one part and ran the tests:
with optional-requires not written, TestTheStoredFormReadsWhereRequiresIsAListOfNames and TestARequirementIsHardUnlessMarkedOptional fail;
with it not read back, both fail;
with the hard-wanter rename removed, TestOneConsumersOptionalRequirementIsAnothersHardOne fails.
The new check that refuses an optional-requires entry the module does not require is covered by the same test.
Checks. merge-check.sh against a throwaway Postgres 18 passes locally: gofmt, go vet, and go test -race over the whole suite, with all 37 listed store tests passing. It used the captured mesh-host copy in testdata/beside, so the genesis-template test that main fails was not hit. The first full run failed once: TestAWalkLetGoByAVerbIsSaidAsPlanMoved gave a NATS "context deadline exceeded" while I was composing in parallel. It passed 3 of 3 runs on its own, and the full rerun passed. On the forge, both required checks on this head read "out of date: judged against trunk cbac703e81, the trunk is now 7b2a9eae8020". Read them green on the recheck before merging.
The notes from 112 about restart-on naming absent ids and contributions still stand. Neither blocks.
Verdict: approve at f0a62c1b473ffeae9c9060df24db0f4127da98a4
Re-review against my review 112, for hq issue 166 (ADR 0330). The rollback finding is resolved, and nothing new came up.
**The stored form now reads on an older controller.** I proved it end to end. This head strict-parsed the files of #259 (grafana, home-assistant) and #36 (ombi), long form included, and marshalled them as registration stores them. The result is a plain `requires` list plus `optional-requires` (grafana: `["route","oidc-client","influxdb-api"]` and `["influxdb-api"]`). I then read those bytes with the controller at main (cbac703) through its own `Manifest.UnmarshalJSON`, the decoder `Inventory.Catalogue()` uses. All three read without error, with the full `requires`, and `UnknownField()` names `optional-requires`. After a rollback, those three modules are therefore left out of every machine by name under ADR 0262: nothing of them changes on their machines, and a condition is raised. The rest of the catalogue resolves. That is the rollback behaviour the mesh already designed for, and much better than every plan failing. (It is not "read as hard", as I suggested in 112. Leaving the module out is the safer of the two.)
**The builder's claim holds: no path in main reads a catalogue file leniently.** At main, every place that reads a module.json from a tree uses the strict `catalogue.ParseManifest`: the builder (`internal/builder/builder.go:224`), `module check` (check.go, modules.go), the merge gate's `shelfWithChange`, and build results (build.go:185, 614, 692, 783). After a rollback, those paths refuse the long form loudly, for that module alone. The lenient `json.Unmarshal` reads are all of stored bytes: `Inventory.Catalogue`, `upgradeFrom` and the bus-provider check in inventory/catalogue.go, and the facts snapshot's `shelfOfFacts` and `graphOfFacts`. With this head, every stored byte is written by `MarshalJSON` in the plain form. `withReferences` reads into a generic map and hands back the bytes unchanged, and `link.ModuleOf` reads only `module`. So keeping the long form in #259 and #36's files is fine.
**Today's mesh still composes byte-identically.** I merged this head into main cbac703 locally; it merges cleanly. I took a fresh facts snapshot (23:09:52Z), composed all 4 machines twice with main and once with the merge, and compared the wire bodies. The resource ids and their order are the same (385, 718, 540 and 361 resources). With the values that differ between two runs of main masked (sealed values, generated secrets, and 5 contents that differ only in a random token), there are 0 differences. The node-engine's validator refuses nothing.
**The refusal now names a hard wanter.** On my copy of the snapshot with influxdb, lidarr and mosquitto unassigned on ace (#259 and #36 laid over it), it reads: nothing in this mesh provides "mqtt-topic", wanted by nodered. It used to name home-assistant. With nodered unassigned as well, ace composes and validates, with the same 4 unbound lines as before. The rename only fires when the first wanter wanted the name optionally, because `because[r]` is set from `Wants()`, so a mesh without optional requirements is untouched. The byte-identity result confirms this.
**The new tests fail without the change.** Each time I removed one part and ran the tests:
- with `optional-requires` not written, TestTheStoredFormReadsWhereRequiresIsAListOfNames and TestARequirementIsHardUnlessMarkedOptional fail;
- with it not read back, both fail;
- with the hard-wanter rename removed, TestOneConsumersOptionalRequirementIsAnothersHardOne fails.
The new check that refuses an `optional-requires` entry the module does not require is covered by the same test.
**Checks.** merge-check.sh against a throwaway Postgres 18 passes locally: gofmt, go vet, and `go test -race` over the whole suite, with all 37 listed store tests passing. It used the captured mesh-host copy in testdata/beside, so the genesis-template test that main fails was not hit. The first full run failed once: TestAWalkLetGoByAVerbIsSaidAsPlanMoved gave a NATS "context deadline exceeded" while I was composing in parallel. It passed 3 of 3 runs on its own, and the full rerun passed. On the forge, both required checks on this head read "out of date: judged against trunk cbac703e817d, the trunk is now 7b2a9eae8020". Read them green on the recheck before merging.
The notes from 112 about restart-on naming absent ids and contributions still stand. Neither blocks.
Merge check at f0a62c1b
Judged: head f0a62c1b473f merged into trunk 454a3db69a1e
mesh/merge-gate: FAIL — head f0a62c1b47 does not merge cleanly into trunk 454a3db69a: in conflict testdata/store-tests (modules: build-agent, mesh-controller)
mesh/repo-check: FAIL — head f0a62c1b47 does not merge cleanly into trunk 454a3db69a: in conflict testdata/store-tests
Delivery plan — builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step
tier 0: mesh-controller
tier 1: build-agent
ace: receives build-agent
g14: receives build-agent
novox: receives mesh-controller, build-agent
shanks: receives build-agent
build-agent keeps data (ADR 0233): what it holds is backed up before it moves
grants step: if this changes what the bus's user list says (a new tool, subject or user), novox is sent that list alone, every build there kept, before build-agent is judged on its first machine
read by no module's build: cmd/mesh-controller/unbound_test.go, internal/catalogue/optional_requirements_test.go, testdata/store-tests
Run by the build seat on g14 as build-1791848235636367152 (builds --log build-1791848235636367152).
head f0a62c1b473f does not merge cleanly into trunk 454a3db69a1e: in conflict testdata/store-tests
**Merge check** at `f0a62c1b`
Judged: head `f0a62c1b473f` merged into trunk `454a3db69a1e`
- `mesh/merge-gate`: **FAIL** — head f0a62c1b473f does not merge cleanly into trunk 454a3db69a1e: in conflict testdata/store-tests (modules: build-agent, mesh-controller)
- `mesh/repo-check`: **FAIL** — head f0a62c1b473f does not merge cleanly into trunk 454a3db69a1e: in conflict testdata/store-tests
**Delivery plan** — builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step
- tier 0: mesh-controller
- tier 1: build-agent
- ace: receives build-agent
- g14: receives build-agent
- novox: receives mesh-controller, build-agent
- shanks: receives build-agent
- build-agent keeps data (ADR 0233): what it holds is backed up before it moves
- grants step: if this changes what the bus's user list says (a new tool, subject or user), novox is sent that list alone, every build there kept, before build-agent is judged on its first machine
- read by no module's build: cmd/mesh-controller/unbound_test.go, internal/catalogue/optional_requirements_test.go, testdata/store-tests
Run by the build seat on g14 as `build-1791848235636367152` (`builds --log build-1791848235636367152`).
```
head f0a62c1b473f does not merge cleanly into trunk 454a3db69a1e: in conflict testdata/store-tests
```
Making every soft dependency a provision made it hard: a mesh without influxdb could not assign
grafana, and one with influxdb in its catalogue would install it for grafana. A requirement may now
be written {"name": ..., "optional": true}. It resolves as a hard one when a provider is assigned or
reachable, and is otherwise absent: never refused, nothing installed for it. Its binding, credential
and only-when-bound files are left out, its ${bound:...|default:...} placeholders take their
defaults, the module check refuses one that could render empty, and plan and node list it unbound.
mesh/merge-gate head 79a3396340a7 merged into trunk 454a3db69a1e — pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; every …
A controller older than optional requirements reads `requires` as names, and Inventory.Catalogue
stops at the first manifest it cannot read: the long form stored inside `requires` would have left
a rollback below this controller composing no machine. Stored, `requires` stays plain and the
optional names sit in `optional-requires`, a key an older controller sets aside, leaving out only
the module carrying it. A refusal of a hard requirement now names a module that wants it hard.
The range-diff between the approved head (f0a62c1b, review 117) and the new head (79a3396), against their respective bases, shows a single difference confined to the testdata/store-tests hunk in the first commit: the new base (main 454a3db) had independently gained its own line (TestAStoredBuildWithASilentMergeableFileIsStillReadAndComposed) since the approval, and the rebase's conflict resolution kept that line alongside the PR's own addition (TestPlanAndNodeListAnOptionalRequirementLeftUnbound). Nothing else differs; the second commit's patch is identical (=) on both sides. This matches the builder's account exactly.
Both test names exist as functions in the new head: TestPlanAndNodeListAnOptionalRequirementLeftUnbound in cmd/mesh-controller/unbound_test.go, and TestAStoredBuildWithASilentMergeableFileIsStillReadAndComposed in internal/inventory/stored_silent_file_test.go.
GOTMPDIR=/var/tmp go build ./..., go vet ./internal/catalogue ./cmd/mesh-controller, and go test -race ./internal/catalogue/ all passed cleanly at 79a3396.
The rebase is clean; the approval at f0a62c1b still holds at the new head.
Verdict: approve at 79a3396340a7f07ada60440a4de176e1e8a04f0f
The range-diff between the approved head (f0a62c1b, review 117) and the new head (79a3396), against their respective bases, shows a single difference confined to the testdata/store-tests hunk in the first commit: the new base (main 454a3db) had independently gained its own line (TestAStoredBuildWithASilentMergeableFileIsStillReadAndComposed) since the approval, and the rebase's conflict resolution kept that line alongside the PR's own addition (TestPlanAndNodeListAnOptionalRequirementLeftUnbound). Nothing else differs; the second commit's patch is identical (=) on both sides. This matches the builder's account exactly.
Both test names exist as functions in the new head: TestPlanAndNodeListAnOptionalRequirementLeftUnbound in cmd/mesh-controller/unbound_test.go, and TestAStoredBuildWithASilentMergeableFileIsStillReadAndComposed in internal/inventory/stored_silent_file_test.go.
GOTMPDIR=/var/tmp go build ./..., go vet ./internal/catalogue ./cmd/mesh-controller, and go test -race ./internal/catalogue/ all passed cleanly at 79a3396.
The rebase is clean; the approval at f0a62c1b still holds at the new head.
Merge check at 79a33963
Judged: head 79a3396340a7 merged into trunk 454a3db69a1e; read beside it: mesh-catalog 39e7a06e6293, mesh-controller-main 454a3db69a1e, mesh-host 7a41e120635b, mesh-tools 954e1a53eef9
mesh/merge-gate: PASS — every machine composes with the change as it did without (4 of 4 compose) (modules: build-agent, mesh-controller)
mesh/repo-check: PASS — its merge-check.sh passed
Delivery plan — builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step
tier 0: mesh-controller
tier 1: build-agent
ace: receives build-agent
g14: receives build-agent
novox: receives mesh-controller, build-agent
shanks: receives build-agent
build-agent keeps data (ADR 0233): what it holds is backed up before it moves
grants step: if this changes what the bus's user list says (a new tool, subject or user), novox is sent that list alone, every build there kept, before build-agent is judged on its first machine
read by no module's build: cmd/mesh-controller/unbound_test.go, internal/catalogue/optional_requirements_test.go, testdata/store-tests
Run by the build seat on novox as build-1791849278130813815 (builds --log build-1791849278130813815).
--- the gate: build-agent, mesh-controller
mesh-controller: ok, 58 tool(s); WARNING: merge-window ask(s) for a setting and do(es) not say whether it is trusted, so it counts as trusted: set at the terminal alone; say "trusted" false where nothing trusts it (novox/hq issue 339); WARNING: controller stay(s) up and say(s) not how it is ready — judged by liveness alone, refused from 2026-11-18 (ADR 0240 rule 8)
long-running resources without health: 1
files asking for a setting without saying whether it is trusted: 1
1 manifest(s) checked. Judged against the seats this binary carries; a claim on one of the mesh's own seats is judged fully at registration, and a seat declared by a module not given here reads as unknown. Identities judged on a 6-character machine name, against the bounds of the providers given here
judging 1 module(s) changed against 4 machine(s), as the snapshot of 2026-10-12T23:40:10Z says they run
composing every machine as the mesh is
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
composing every machine with the change
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
merge gate: PASS — every machine composes with the change as it did without (4 of 4 compose)
judged by controller development build against the facts of 2026-10-12T23:40:10Z
modules the change moves:
- mesh-controller (changed)
machines:
- a machine (f35): composes
- holds mesh-dns-resolver (idt): composes
- holds git, holds mesh-artifact-store, holds mesh-broker, holds mesh-catalog, holds mesh-delivery, holds mesh-dns-resolver, holds mesh-store, holds mesh-vault, holds npm-package-registry, the control node, the hub (ygcyr): composes
- a machine (yvbpsm): composes
a merge would rebuild 2 module(s) in 2 tier(s): build-agent, mesh-controller
notes:
- cmd/mesh-controller/unbound_test.go, internal/catalogue/optional_requirements_test.go and testdata/store-tests read by no module's build, and rebuild nothing (issue 280)
--- its merge-check.sh (go toolchain)
ok github.com/novox/mesh-controller/cmd/mesh-builder 1.100s
ok github.com/novox/mesh-controller/cmd/mesh-controller 213.437s
ok github.com/novox/mesh-controller/examples/objectstore-provisioner 1.020s
ok github.com/novox/mesh-controller/examples/postgres-provisioner 1.046s
ok github.com/novox/mesh-controller/examples/redis-provisioner 1.034s
ok github.com/novox/mesh-controller/examples/route-proxy 1.128s
ok github.com/novox/mesh-controller/internal/artifacts 1.063s
? github.com/novox/mesh-controller/internal/beside [no test files]
ok github.com/novox/mesh-controller/internal/broker 5.158s
ok github.com/novox/mesh-controller/internal/builder 59.570s
ok github.com/novox/mesh-controller/internal/catalogue 9.807s
ok github.com/novox/mesh-controller/internal/conditions 2.783s
ok github.com/novox/mesh-controller/internal/envfile 1.051s
ok github.com/novox/mesh-controller/internal/facts 1.039s
ok github.com/novox/mesh-controller/internal/identity 6.231s
ok github.com/novox/mesh-controller/internal/inventory 52.864s
ok github.com/novox/mesh-controller/internal/lease 17.766s
ok github.com/novox/mesh-controller/internal/licences 4.880s
ok github.com/novox/mesh-controller/internal/licences/adapters 1.023s
ok github.com/novox/mesh-controller/internal/link 68.248s
ok github.com/novox/mesh-controller/internal/lint 2.609s
ok github.com/novox/mesh-controller/internal/outward 1.046s
ok github.com/novox/mesh-controller/internal/overlay 1.036s
ok github.com/novox/mesh-controller/internal/secrets 1.526s
ok github.com/novox/mesh-controller/internal/store 3.714s
ok github.com/novox/mesh-controller/internal/testbus 1.225s
ok github.com/novox/mesh-controller/internal/token 1.019s
store tests, each run against the store:
./cmd/mesh-controller --- PASS: TestAStopWithoutAMergeWalksTheOthersAgain (0.25s)
./cmd/mesh-controller --- PASS: TestAStopWithoutAMergeEndsTheMergesThatContainIt (0.29s)
./cmd/mesh-controller --- PASS: TestAStopWalksTheOthersAgainInTheOpenBatch (0.24s)
./cmd/mesh-controller --- PASS: TestAStopNamesWhatStaysOnTheMachinesAndWhatStillCarriesIt (0.23s)
./cmd/mesh-controller --- PASS: TestAStopWithoutAMergeLeavesItOutOfABatch (0.21s)
./cmd/mesh-controller --- PASS: TestSavingPlansAndMovingMergesIsOneAct (0.18s)
./cmd/mesh-controller --- PASS: TestAPlanFollowedFromTheStoreAnswersWhenItMoves (2.96s)
./cmd/mesh-controller --- PASS: TestNodeShowAndStatusReadTheLastSendFromTheStore (0.27s)
./cmd/mesh-controller --- PASS: TestTheConditionsVerbReadsTheLooksTheBucketNoLongerHolds (0.16s)
./cmd/mesh-controller --- PASS: TestAWalkLetGoAndStoppedByVerbsIsSaidStartedAndEnded (0.43s)
./cmd/mesh-controller --- PASS: TestAWalkStoppedOrClosedByAPersonIsSaidEndedStopped (0.35s)
./cmd/mesh-controller --- PASS: TestAWalkRetriedByAPersonIsSaidStartedAgain (0.46s)
./cmd/mesh-controller --- PASS: TestABacklogReleasedByAPersonIsSaidStarted (0.49s)
./cmd/mesh-controller --- PASS: TestAWalkEndedByCancelOrClearIsSaidEnded (0.78s)
./cmd/mesh-controller --- PASS: TestAWalkRestartedByARebuildIsSaidStarted (0.37s)
./cmd/mesh-controller --- PASS: TestAWalkStoppedByHandAndRetriedEndsFailedWhenItFailsAgain (0.28s)
./cmd/mesh-controller --- PASS: TestReplay237 (0.32s)
./cmd/mesh-controller --- PASS: TestAnUnassignThatLeavesItsNodeUnresolvedSaysSoOnceInItsAnswer (0.22s)
./cmd/mesh-controller --- PASS: TestAnUnassignThatLeavesItsNodeResolvedSaysNothingOfIt (0.22s)
./cmd/mesh-controller --- PASS: TestAnAssignThatLeavesItsNodeUnresolvedSaysOnlyThePlansRefusal (0.23s)
./cmd/mesh-controller --- PASS: TestADirectoryOwnersKeyIsRefusedThroughAVerb (0.28s)
./cmd/mesh-controller --- PASS: TestTheSendMakesNoValueForAnOutsideSecretNobodyGave (0.34s)
./cmd/mesh-controller --- PASS: TestAQuestionComposesWithoutAnOutsideSecretNobodyGave (0.34s)
./cmd/mesh-controller --- PASS: TestReplay201 (0.18s)
./cmd/mesh-controller --- PASS: TestARollbackWhoseSendWasRefusedIsCompletedByTheNextSend (0.17s)
./cmd/mesh-controller --- PASS: TestAGateJudgesTheOtherMovesBesideAKeptCore (0.23s)
./cmd/mesh-controller --- PASS: TestReplay401 (0.45s)
./cmd/mesh-controller --- PASS: TestReplay201ThePlansSend (0.36s)
./cmd/mesh-controller --- PASS: TestPlanAndNodeListAnOptionalRequirementLeftUnbound (0.46s)
./internal/inventory --- PASS: TestTheLastSendSaysWhatTheMachineAnswered (0.09s)
./internal/inventory --- PASS: TestAReportIsTheLastSendsAnswerOnlyWhenItIsAboutThatSend (0.09s)
./internal/inventory --- PASS: TestAnIssuedValueIsSealedToEveryHolderOnceAndNotAgain (0.14s)
./internal/inventory --- PASS: TestAnIssuedValueIsSealedAgainAfterAKeyChangeOrAnAcceptance (0.09s)
./internal/inventory --- PASS: TestAConditionsLooksAreKeptBesideTheBucket (0.11s)
./internal/inventory --- PASS: TestAPlanIdHoldingAQuoteIsReadAsAPlainId (0.10s)
./internal/inventory --- PASS: TestAWalksStartIsKeptAndARefusedSaveLeavesItUnsaid (0.09s)
./internal/inventory --- PASS: TestAWalksRestIsReadFromEachMachinesOwnSend (0.08s)
./internal/inventory --- PASS: TestAStoredBuildWithASilentMergeableFileIsStillReadAndComposed (0.07s)
**Merge check** at `79a33963`
Judged: head `79a3396340a7` merged into trunk `454a3db69a1e`; read beside it: mesh-catalog `39e7a06e6293`, mesh-controller-main `454a3db69a1e`, mesh-host `7a41e120635b`, mesh-tools `954e1a53eef9`
- `mesh/merge-gate`: **PASS** — every machine composes with the change as it did without (4 of 4 compose) (modules: build-agent, mesh-controller)
- `mesh/repo-check`: **PASS** — its merge-check.sh passed
**Delivery plan** — builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step
- tier 0: mesh-controller
- tier 1: build-agent
- ace: receives build-agent
- g14: receives build-agent
- novox: receives mesh-controller, build-agent
- shanks: receives build-agent
- build-agent keeps data (ADR 0233): what it holds is backed up before it moves
- grants step: if this changes what the bus's user list says (a new tool, subject or user), novox is sent that list alone, every build there kept, before build-agent is judged on its first machine
- read by no module's build: cmd/mesh-controller/unbound_test.go, internal/catalogue/optional_requirements_test.go, testdata/store-tests
Run by the build seat on novox as `build-1791849278130813815` (`builds --log build-1791849278130813815`).
```
--- the gate: build-agent, mesh-controller
mesh-controller: ok, 58 tool(s); WARNING: merge-window ask(s) for a setting and do(es) not say whether it is trusted, so it counts as trusted: set at the terminal alone; say "trusted" false where nothing trusts it (novox/hq issue 339); WARNING: controller stay(s) up and say(s) not how it is ready — judged by liveness alone, refused from 2026-11-18 (ADR 0240 rule 8)
long-running resources without health: 1
files asking for a setting without saying whether it is trusted: 1
1 manifest(s) checked. Judged against the seats this binary carries; a claim on one of the mesh's own seats is judged fully at registration, and a seat declared by a module not given here reads as unknown. Identities judged on a 6-character machine name, against the bounds of the providers given here
judging 1 module(s) changed against 4 machine(s), as the snapshot of 2026-10-12T23:40:10Z says they run
composing every machine as the mesh is
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
composing every machine with the change
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
merge gate: PASS — every machine composes with the change as it did without (4 of 4 compose)
judged by controller development build against the facts of 2026-10-12T23:40:10Z
modules the change moves:
- mesh-controller (changed)
machines:
- a machine (f35): composes
- holds mesh-dns-resolver (idt): composes
- holds git, holds mesh-artifact-store, holds mesh-broker, holds mesh-catalog, holds mesh-delivery, holds mesh-dns-resolver, holds mesh-store, holds mesh-vault, holds npm-package-registry, the control node, the hub (ygcyr): composes
- a machine (yvbpsm): composes
a merge would rebuild 2 module(s) in 2 tier(s): build-agent, mesh-controller
notes:
- cmd/mesh-controller/unbound_test.go, internal/catalogue/optional_requirements_test.go and testdata/store-tests read by no module's build, and rebuild nothing (issue 280)
--- its merge-check.sh (go toolchain)
ok github.com/novox/mesh-controller/cmd/mesh-builder 1.100s
ok github.com/novox/mesh-controller/cmd/mesh-controller 213.437s
ok github.com/novox/mesh-controller/examples/objectstore-provisioner 1.020s
ok github.com/novox/mesh-controller/examples/postgres-provisioner 1.046s
ok github.com/novox/mesh-controller/examples/redis-provisioner 1.034s
ok github.com/novox/mesh-controller/examples/route-proxy 1.128s
ok github.com/novox/mesh-controller/internal/artifacts 1.063s
? github.com/novox/mesh-controller/internal/beside [no test files]
ok github.com/novox/mesh-controller/internal/broker 5.158s
ok github.com/novox/mesh-controller/internal/builder 59.570s
ok github.com/novox/mesh-controller/internal/catalogue 9.807s
ok github.com/novox/mesh-controller/internal/conditions 2.783s
ok github.com/novox/mesh-controller/internal/envfile 1.051s
ok github.com/novox/mesh-controller/internal/facts 1.039s
ok github.com/novox/mesh-controller/internal/identity 6.231s
ok github.com/novox/mesh-controller/internal/inventory 52.864s
ok github.com/novox/mesh-controller/internal/lease 17.766s
ok github.com/novox/mesh-controller/internal/licences 4.880s
ok github.com/novox/mesh-controller/internal/licences/adapters 1.023s
ok github.com/novox/mesh-controller/internal/link 68.248s
ok github.com/novox/mesh-controller/internal/lint 2.609s
ok github.com/novox/mesh-controller/internal/outward 1.046s
ok github.com/novox/mesh-controller/internal/overlay 1.036s
ok github.com/novox/mesh-controller/internal/secrets 1.526s
ok github.com/novox/mesh-controller/internal/store 3.714s
ok github.com/novox/mesh-controller/internal/testbus 1.225s
ok github.com/novox/mesh-controller/internal/token 1.019s
store tests, each run against the store:
./cmd/mesh-controller --- PASS: TestAStopWithoutAMergeWalksTheOthersAgain (0.25s)
./cmd/mesh-controller --- PASS: TestAStopWithoutAMergeEndsTheMergesThatContainIt (0.29s)
./cmd/mesh-controller --- PASS: TestAStopWalksTheOthersAgainInTheOpenBatch (0.24s)
./cmd/mesh-controller --- PASS: TestAStopNamesWhatStaysOnTheMachinesAndWhatStillCarriesIt (0.23s)
./cmd/mesh-controller --- PASS: TestAStopWithoutAMergeLeavesItOutOfABatch (0.21s)
./cmd/mesh-controller --- PASS: TestSavingPlansAndMovingMergesIsOneAct (0.18s)
./cmd/mesh-controller --- PASS: TestAPlanFollowedFromTheStoreAnswersWhenItMoves (2.96s)
./cmd/mesh-controller --- PASS: TestNodeShowAndStatusReadTheLastSendFromTheStore (0.27s)
./cmd/mesh-controller --- PASS: TestTheConditionsVerbReadsTheLooksTheBucketNoLongerHolds (0.16s)
./cmd/mesh-controller --- PASS: TestAWalkLetGoAndStoppedByVerbsIsSaidStartedAndEnded (0.43s)
./cmd/mesh-controller --- PASS: TestAWalkStoppedOrClosedByAPersonIsSaidEndedStopped (0.35s)
./cmd/mesh-controller --- PASS: TestAWalkRetriedByAPersonIsSaidStartedAgain (0.46s)
./cmd/mesh-controller --- PASS: TestABacklogReleasedByAPersonIsSaidStarted (0.49s)
./cmd/mesh-controller --- PASS: TestAWalkEndedByCancelOrClearIsSaidEnded (0.78s)
./cmd/mesh-controller --- PASS: TestAWalkRestartedByARebuildIsSaidStarted (0.37s)
./cmd/mesh-controller --- PASS: TestAWalkStoppedByHandAndRetriedEndsFailedWhenItFailsAgain (0.28s)
./cmd/mesh-controller --- PASS: TestReplay237 (0.32s)
./cmd/mesh-controller --- PASS: TestAnUnassignThatLeavesItsNodeUnresolvedSaysSoOnceInItsAnswer (0.22s)
./cmd/mesh-controller --- PASS: TestAnUnassignThatLeavesItsNodeResolvedSaysNothingOfIt (0.22s)
./cmd/mesh-controller --- PASS: TestAnAssignThatLeavesItsNodeUnresolvedSaysOnlyThePlansRefusal (0.23s)
./cmd/mesh-controller --- PASS: TestADirectoryOwnersKeyIsRefusedThroughAVerb (0.28s)
./cmd/mesh-controller --- PASS: TestTheSendMakesNoValueForAnOutsideSecretNobodyGave (0.34s)
./cmd/mesh-controller --- PASS: TestAQuestionComposesWithoutAnOutsideSecretNobodyGave (0.34s)
./cmd/mesh-controller --- PASS: TestReplay201 (0.18s)
./cmd/mesh-controller --- PASS: TestARollbackWhoseSendWasRefusedIsCompletedByTheNextSend (0.17s)
./cmd/mesh-controller --- PASS: TestAGateJudgesTheOtherMovesBesideAKeptCore (0.23s)
./cmd/mesh-controller --- PASS: TestReplay401 (0.45s)
./cmd/mesh-controller --- PASS: TestReplay201ThePlansSend (0.36s)
./cmd/mesh-controller --- PASS: TestPlanAndNodeListAnOptionalRequirementLeftUnbound (0.46s)
./internal/inventory --- PASS: TestTheLastSendSaysWhatTheMachineAnswered (0.09s)
./internal/inventory --- PASS: TestAReportIsTheLastSendsAnswerOnlyWhenItIsAboutThatSend (0.09s)
./internal/inventory --- PASS: TestAnIssuedValueIsSealedToEveryHolderOnceAndNotAgain (0.14s)
./internal/inventory --- PASS: TestAnIssuedValueIsSealedAgainAfterAKeyChangeOrAnAcceptance (0.09s)
./internal/inventory --- PASS: TestAConditionsLooksAreKeptBesideTheBucket (0.11s)
./internal/inventory --- PASS: TestAPlanIdHoldingAQuoteIsReadAsAPlainId (0.10s)
./internal/inventory --- PASS: TestAWalksStartIsKeptAndARefusedSaveLeavesItUnsaid (0.09s)
./internal/inventory --- PASS: TestAWalksRestIsReadFromEachMachinesOwnSend (0.08s)
./internal/inventory --- PASS: TestAStoredBuildWithASilentMergeableFileIsStillReadAndComposed (0.07s)
```
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
What
A manifest's requirement may be written
{"name": "influxdb-api", "optional": true}(ADR 0330 §1). A bare name stays hard.internal/catalogue/resolve.go): a name only optional consumers want is settled after the walk. It binds exactly as a hard requirement when a provider is assigned or reachable (§2). Otherwise it is absent: not refused, and nothing is installed for it, not even the only catalogue candidate, on this machine or elsewhere (§3). A name another module wants hard (including one pulled in later) still resolves and installs as before.Resolution.Absentlists each absent requirement by consumer.declaration.go,bound_into_files.go):${bound:<provision>:<key>|default:<value>}renders its default while the requirement is absent (§4). A file markedonly-when-bound: <requirement>is left out while it is absent (§5), and the field is dropped from every resource before sending. Binding, credential and grant files of an absent requirement are not composed (§6).optional_requirements.go): refuses an optional${bound:…}placeholder without a default outside an only-when-bound file, and a${secret:…}of an optional requirement outside one. It also refusesonly-when-boundon a hard or unrequired name or on a non-file, and a default on a hard requirement.cmd/mesh-controller/unbound.go,plan.go,nodes.go): each listunbound <requirement>, optional for <consumer>. Forplan --json/--diffthe line goes to stderr, which the verb answer carries, because the declaration envelope is parsed strictly by the node-engine (§7).modulesmarks a requirement(optional).Why
Issue 166: soft dependencies became hard provisions, so grafana could not be assigned on a mesh without influxdb, and with influxdb in the catalogue it would have been installed for grafana.
How it was tested
internal/catalogue/optional_requirements_test.gocovers:cmd/mesh-controller/unbound_test.go(store test, added totestdata/store-tests): with grafana assigned and influxdb only in the catalogue,plan laptopandnode show laptopboth sayinfluxdb-apiis unbound and influxdb is not installed. Once influxdb is assigned on anchor, grafana binds to it with no act on grafana.MESH_TEST_POSTGRES=… sh merge-check.shpasses locally.Design limits chosen
only-when-boundapplies to files only (ADR 0330 §5 also says "contribution"). Acontributesentry to an optional requirement is already composed only while it binds, because it goes to whoever answers it. No seat contribution in the catalogue needs the mark yet.|default:}is not a placeholder, and the sweep refuses it as a misspelling.Fixes issue 166.
The catalogue PRs (mesh-catalog: grafana, home-assistant; mesh-media-catalog: ombi) depend on this one shipping first.
Delivery
novox/mesh-controller@79a3396340a7— delivered since 2026-10-13T00:15:31ZDelivery plan — builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step
Machines
Transitions
The commit's note under
refs/notes/mesh-plankeeps every transition:git log --notes=mesh-plan.Merge check at
bc81029aJudged: head
bc81029a030amerged into trunk56d6313262e8; read beside it: mesh-catalog8f80cd471634, mesh-controller-main56d6313262e8, mesh-host7a41e120635b, mesh-tools954e1a53eef9mesh/merge-gate: PASS — every machine composes with the change as it did without (4 of 4 compose) (modules: build-agent, mesh-controller)mesh/repo-check: FAIL — its merge-check.sh failed: --- FAIL: TestTheInstallersFirstUserListIsWhatTheControllerWouldCompose (0.00s)Delivery plan — builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step
Run by the build seat on shanks as
build-1791844826150324029(builds --log build-1791844826150324029).Verdict: request changes at
bc81029a03Independent review for hq issue 166 (ADR 0330). I am asking for one change: the form a registered manifest is stored in (finding 4). Everything else I checked holds, and the evidence is below.
1. Today's mesh composes byte-identically. Proven. I merged this head into main
56d6313locally; it merges cleanly. I took the facts snapshot of 2026-10-12 22:43:48Z and composed every machine twice with main and once with the merge. Each run used the gate's own path (raiseFromFacts, then composedAndValidated, thensendable.Body()) against a throwaway Postgres. Both controllers produced the same resource ids in the same order on all 4 machines: 385, 718, 540 and 361 resources. The only bytes that differ are those that also differ between two runs of main: sealed values, generated secrets, and 5 file contents whose only difference is a random token. With those masked, main and the merge are identical. The node-engine's validator refuses nothing in either.2. Resolution works as decided. The tests cover each case, and I mutated the code to show each test catches its case. Optional with no provider: absent, nothing installed, on both passes. Optional with a provider elsewhere: binds as a hard requirement would. One consumer's optional want and another's hard want: still refused, or installed when a module pulled in later wants it hard. On real data, I made a copy of the snapshot with influxdb, lidarr, mosquitto and nodered unassigned on ace, and laid catalogue #259 and media #36 over it. ace composes and validates, and
planlists four unbound lines. With nodered left assigned, mqtt-topic is refused because nodered wants it hard. That is correct.3.
only-when-boundis stripped before sending.withoutAbsentdrops the field from every resource it keeps. In every composed body (the snapshot as it is, with #259 and #36, and the absent copy) no resource carriesonly-when-bound.4. Rollback to an older controller: not acceptable as written. Keep a plain-name copy.
Inventory.Catalogue()unmarshals every stored manifest and returns an error on the first one it cannot read. I checked the controller at main against both forms:cannot unmarshal object into manifestFields.requires.1 of type string;{"requires":["a","b"],"optional-requires":["b"]}reads cleanly, because the extra key goes tounknown.So once #259 or #36 is registered, a controller older than this one cannot resolve any machine. Every plan and every push fails, and the controller cannot heal itself. A merge gate catches a revert, but a person's
pushor a rebuild of an older controller build does not go through one, and on 2026-10-02 an older controller build did reach the control node.The fix is small and stays inside the controller: have
MarshalJSONwriterequiresas plain names plus anoptional-requireslist, and haveUnmarshalJSONread that as well as the long form. The catalogue keeps the long form in its source files. An older controller would then treat the optional requirements as hard, which composes today, because each one is bound on ace. At worst it refuses or installs for that one module, as it did before ADR 0330. That is far less than every machine failing to compose. Please add a test that stores a manifest with an optional requirement and reads it back through the old field shape.5. Grafana's data source on ace is covered on #259: it keeps its uid, and the files move with the old paths removed as undeclared.
6. The tests fail without the changes. I took out each part of the change in turn and ran the tests:
withoutAbsentturned off: 2 tests fail;OptionalProblemstaken out of the module check: the module-check test fails;unboundLinesemptied: TestPlanAndNodeListAnOptionalRequirementLeftUnbound fails, for both plan and node.Checks. I read every status on this head.
mesh/merge-gatepassed at 22:46 against trunk56d6313("builds build-agent, mesh-controller → ace, g14, novox, shanks"). It has been marked out of date since trunk moved tocbac703.mesh/repo-checkfailed at 22:46 onTestTheInstallersFirstUserListIsWhatTheControllerWouldCompose, and this change did not cause it. mesh-host 37947ca (hq issue 146, ADR 0331) removedexamples/foundation-first-node-nats.lock, butinternal/broker/genesis_template_test.goon controller main (cbac703too) still reads that file. Against mesh-host main as the clone beside the check, main fails the same way. Every controller pull request will be red on this until that test follows mesh-host. Locally, without that clone (the captured copy in testdata/beside), merge-check.sh against a throwaway Postgres 18 passes: gofmt, go vet, andgo test -raceover the whole suite, with all 37 listed store tests passing, including the new one. The first full run failed once oninternal/leaseTestKeepingRenewsAcrossManyAges ("context deadline exceeded" under load). That package passed 3 of 3 runs on its own, and the second full run passed. It is a flake unrelated to this change.Not blocking:
because[want], which is the first module that queued the name. On the absent copy it said: nothing in this mesh provides "mqtt-topic", wanted by home-assistant. home-assistant wants it only optionally, and nodered is the module that wants it hard. Name a hard wanter when there is one.only-when-boundfile, or the bound and secret ids of an absent requirement. The node-engine accepts this and reads an empty digest, so binding later restarts the container, which is the behaviour we want. A line in the module-check comment would stop the next reader wondering.only-when-bound. The check refuses the field on anything but a file. A contribution to an absent requirement has no provider to receive it, so the result is the same, but the comment in OptionalProblems should say so.Review 112 addressed at
f0a62c1.MarshalJSONnow writesrequiresas a plain list of names, with the optional ones in a separate key,optional-requires.UnmarshalJSONreads both shapes: the{name, optional}entry and the separate list. The module check refuses anoptional-requiresentry the module does not require.TestTheStoredFormReadsWhereRequiresIsAListOfNamesunmarshals the stored form into main'sstruct{ Requires []string }. It also reads it back here, both leniently and throughParseManifest.catalogue.Manifest. The error is nil and requires is[route influxdb-api]. The unknownoptional-requireskey is set aside, so only that module is left out and the catalogue still reads.Catalogue,Catalogued, snapshots for the gate,upgradeFrom), and the store always holds this controller'sMarshalJSONform. A catalogue file is only read through the strictParseManifest. On an older controller that refuses the file whichever form it uses (a type error or an unknown key), and only that one registration. So the files keep ADR 0330's{name, optional}form.wanted by telegraf, tested). Before, it named the optional consumer.only-when-boundon contributions: not built. Acontributesentry is keyed by the requirement and goes only to whoever answers it, so it is already composed only while the requirement is bound. A seat contribution is not tied to a requirement, and none in the catalogues needs it. I can add it when one does.MESH_TEST_POSTGRES=… sh merge-check.shexit 0, including the store testTestPlanAndNodeListAnOptionalRequirementLeftUnbound. I judged it locally because repo-check is red on main for the unrelated genesis bundle.Merge check at
f0a62c1bJudged: head
f0a62c1b473fmerged into trunkcbac703e817d; read beside it: mesh-catalog8f80cd471634, mesh-controller-maincbac703e817d, mesh-host7a41e120635b, mesh-tools954e1a53eef9mesh/merge-gate: PASS — every machine composes with the change as it did without (4 of 4 compose) (modules: build-agent, mesh-controller)mesh/repo-check: FAIL — its merge-check.sh failed: --- FAIL: TestTheInstallersFirstUserListIsWhatTheControllerWouldCompose (0.00s)Delivery plan — builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step
Run by the build seat on shanks as
build-1791847135848669029(builds --log build-1791847135848669029).Out of date when it landed: out of date: judged against trunk
cbac703e81, the trunk is now 7b2a9eae8020; judged again on a recheck or when it is mergedVerdict: approve at
f0a62c1b47Re-review against my review 112, for hq issue 166 (ADR 0330). The rollback finding is resolved, and nothing new came up.
The stored form now reads on an older controller. I proved it end to end. This head strict-parsed the files of #259 (grafana, home-assistant) and #36 (ombi), long form included, and marshalled them as registration stores them. The result is a plain
requireslist plusoptional-requires(grafana:["route","oidc-client","influxdb-api"]and["influxdb-api"]). I then read those bytes with the controller at main (cbac703) through its ownManifest.UnmarshalJSON, the decoderInventory.Catalogue()uses. All three read without error, with the fullrequires, andUnknownField()namesoptional-requires. After a rollback, those three modules are therefore left out of every machine by name under ADR 0262: nothing of them changes on their machines, and a condition is raised. The rest of the catalogue resolves. That is the rollback behaviour the mesh already designed for, and much better than every plan failing. (It is not "read as hard", as I suggested in 112. Leaving the module out is the safer of the two.)The builder's claim holds: no path in main reads a catalogue file leniently. At main, every place that reads a module.json from a tree uses the strict
catalogue.ParseManifest: the builder (internal/builder/builder.go:224),module check(check.go, modules.go), the merge gate'sshelfWithChange, and build results (build.go:185, 614, 692, 783). After a rollback, those paths refuse the long form loudly, for that module alone. The lenientjson.Unmarshalreads are all of stored bytes:Inventory.Catalogue,upgradeFromand the bus-provider check in inventory/catalogue.go, and the facts snapshot'sshelfOfFactsandgraphOfFacts. With this head, every stored byte is written byMarshalJSONin the plain form.withReferencesreads into a generic map and hands back the bytes unchanged, andlink.ModuleOfreads onlymodule. So keeping the long form in #259 and #36's files is fine.Today's mesh still composes byte-identically. I merged this head into main
cbac703locally; it merges cleanly. I took a fresh facts snapshot (23:09:52Z), composed all 4 machines twice with main and once with the merge, and compared the wire bodies. The resource ids and their order are the same (385, 718, 540 and 361 resources). With the values that differ between two runs of main masked (sealed values, generated secrets, and 5 contents that differ only in a random token), there are 0 differences. The node-engine's validator refuses nothing.The refusal now names a hard wanter. On my copy of the snapshot with influxdb, lidarr and mosquitto unassigned on ace (#259 and #36 laid over it), it reads: nothing in this mesh provides "mqtt-topic", wanted by nodered. It used to name home-assistant. With nodered unassigned as well, ace composes and validates, with the same 4 unbound lines as before. The rename only fires when the first wanter wanted the name optionally, because
because[r]is set fromWants(), so a mesh without optional requirements is untouched. The byte-identity result confirms this.The new tests fail without the change. Each time I removed one part and ran the tests:
optional-requiresnot written, TestTheStoredFormReadsWhereRequiresIsAListOfNames and TestARequirementIsHardUnlessMarkedOptional fail;The new check that refuses an
optional-requiresentry the module does not require is covered by the same test.Checks. merge-check.sh against a throwaway Postgres 18 passes locally: gofmt, go vet, and
go test -raceover the whole suite, with all 37 listed store tests passing. It used the captured mesh-host copy in testdata/beside, so the genesis-template test that main fails was not hit. The first full run failed once: TestAWalkLetGoByAVerbIsSaidAsPlanMoved gave a NATS "context deadline exceeded" while I was composing in parallel. It passed 3 of 3 runs on its own, and the full rerun passed. On the forge, both required checks on this head read "out of date: judged against trunkcbac703e81, the trunk is now 7b2a9eae8020". Read them green on the recheck before merging.The notes from 112 about restart-on naming absent ids and contributions still stand. Neither blocks.
Merge check at
f0a62c1bJudged: head
f0a62c1b473fmerged into trunk454a3db69a1emesh/merge-gate: FAIL — headf0a62c1b47does not merge cleanly into trunk454a3db69a: in conflict testdata/store-tests (modules: build-agent, mesh-controller)mesh/repo-check: FAIL — headf0a62c1b47does not merge cleanly into trunk454a3db69a: in conflict testdata/store-testsDelivery plan — builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step
Run by the build seat on g14 as
build-1791848235636367152(builds --log build-1791848235636367152).Making every soft dependency a provision made it hard: a mesh without influxdb could not assign grafana, and one with influxdb in its catalogue would install it for grafana. A requirement may now be written {"name": ..., "optional": true}. It resolves as a hard one when a provider is assigned or reachable, and is otherwise absent: never refused, nothing installed for it. Its binding, credential and only-when-bound files are left out, its ${bound:...|default:...} placeholders take their defaults, the module check refuses one that could render empty, and plan and node list it unbound.f0a62c1b47to79a3396340Verdict: approve at
79a3396340The range-diff between the approved head (
f0a62c1b, review 117) and the new head (79a3396), against their respective bases, shows a single difference confined to the testdata/store-tests hunk in the first commit: the new base (main454a3db) had independently gained its own line (TestAStoredBuildWithASilentMergeableFileIsStillReadAndComposed) since the approval, and the rebase's conflict resolution kept that line alongside the PR's own addition (TestPlanAndNodeListAnOptionalRequirementLeftUnbound). Nothing else differs; the second commit's patch is identical (=) on both sides. This matches the builder's account exactly.Both test names exist as functions in the new head: TestPlanAndNodeListAnOptionalRequirementLeftUnbound in cmd/mesh-controller/unbound_test.go, and TestAStoredBuildWithASilentMergeableFileIsStillReadAndComposed in internal/inventory/stored_silent_file_test.go.
GOTMPDIR=/var/tmp go build ./..., go vet ./internal/catalogue ./cmd/mesh-controller, and go test -race ./internal/catalogue/ all passed cleanly at
79a3396.The rebase is clean; the approval at
f0a62c1bstill holds at the new head.Merge check at
79a33963Judged: head
79a3396340a7merged into trunk454a3db69a1e; read beside it: mesh-catalog39e7a06e6293, mesh-controller-main454a3db69a1e, mesh-host7a41e120635b, mesh-tools954e1a53eef9mesh/merge-gate: PASS — every machine composes with the change as it did without (4 of 4 compose) (modules: build-agent, mesh-controller)mesh/repo-check: PASS — its merge-check.sh passedDelivery plan — builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step
Run by the build seat on novox as
build-1791849278130813815(builds --log build-1791849278130813815).