Fixes the backup half of novox/hq issue 416 (backup retention and the container runtime's log rotation cannot become preferences yet). The log-rotation half needs a decision and is not in this pull request; see the note on 416.
Cause
The retention was a Go literal in modules/restic/cmd/restic-backups/backups.go (Keep = {14, 8, 6}), so no setting could reach it. The controller's existing mechanism (ADR 0262 preferences filling ${setting:} into a file, as zfs's scrub schedule does) supports it; no controller change is needed.
Change
module.json: three preferences, keep-daily 14, keep-weekly 8, keep-monthly 6 (today's values), and a new file ${dir:state}/retention.conf (daily=, weekly=, monthly=) that asks for them. The file is trusted: true: lowering a count deletes restore points, so the value is set at the terminal alone (issue 339).
retention.go: the night reads that file. Each value must be a count from 1 to 9999 with no leading zero; an unknown, repeated or missing key, or a line that is not key=value, is refused. A refused file means nothing is thinned, every restore point is kept, and the module says which setting is wrong in its journal. No raw value ever reaches restic's arguments (issue 346).
A missing file (a machine placed by the older definition) keeps today's 14/8/6.
Tests (written first)
retention_test.go: against main with stubs, three of the four fail (TestANightThinsByTheRetentionTheMeshWrote, TestARetentionThatIsNotACountThinsNothingAndSaysWhich, TestTheManifestsDefaultsAreTodaysRetention); TestWithoutTheFileANightKeepsTodaysRetention passes on main by design. The manifest test fills the file with the declared defaults and checks that it parses to exactly today's retention.
Run here: go vet and go test -race of modules/restic; merge-check.sh with MESH_GATE set to a controller built from mesh-controller main 11ffab8 and MESH_CHECK_CHANGED set to this diff (exit 0, restic: ok, no untrusted-setting warning); the words check (cd checks/words && go run . ../.., clean).
What changes on a machine with the defaults
mesh-controller.plan cannot show an unmerged branch, so this is stated, not measured. restic runs on ace, g14, novox and shanks. On each:
a new file, <restic state>/retention.conf, holding daily=14, weekly=8, monthly=6;
the restic tools bundle is rebuilt and restarts with the new binary.
The nightly restic forget line is byte for byte the same (14/8/6), so no restore point is kept or dropped differently. No other file, unit or module changes.
Not touched: modules/mesh-delivery, modules/mesh-cli (open #198, #199).
Fixes the backup half of novox/hq issue 416 (backup retention and the container runtime's log rotation cannot become preferences yet). The log-rotation half needs a decision and is not in this pull request; see the note on 416.
## Cause
The retention was a Go literal in `modules/restic/cmd/restic-backups/backups.go` (`Keep = {14, 8, 6}`), so no setting could reach it. The controller's existing mechanism (ADR 0262 preferences filling `${setting:}` into a file, as zfs's scrub schedule does) supports it; no controller change is needed.
## Change
- `module.json`: three preferences, `keep-daily` 14, `keep-weekly` 8, `keep-monthly` 6 (today's values), and a new file `${dir:state}/retention.conf` (`daily=`, `weekly=`, `monthly=`) that asks for them. The file is `trusted: true`: lowering a count deletes restore points, so the value is set at the terminal alone (issue 339).
- `retention.go`: the night reads that file. Each value must be a count from 1 to 9999 with no leading zero; an unknown, repeated or missing key, or a line that is not key=value, is refused. A refused file means **nothing is thinned, every restore point is kept**, and the module says which setting is wrong in its journal. No raw value ever reaches restic's arguments (issue 346).
- A missing file (a machine placed by the older definition) keeps today's 14/8/6.
## Tests (written first)
`retention_test.go`: against main with stubs, three of the four fail (`TestANightThinsByTheRetentionTheMeshWrote`, `TestARetentionThatIsNotACountThinsNothingAndSaysWhich`, `TestTheManifestsDefaultsAreTodaysRetention`); `TestWithoutTheFileANightKeepsTodaysRetention` passes on main by design. The manifest test fills the file with the declared defaults and checks that it parses to exactly today's retention.
Run here: `go vet` and `go test -race` of modules/restic; `merge-check.sh` with `MESH_GATE` set to a controller built from mesh-controller main 11ffab8 and `MESH_CHECK_CHANGED` set to this diff (exit 0, `restic: ok`, no untrusted-setting warning); the words check (`cd checks/words && go run . ../..`, clean).
## What changes on a machine with the defaults
`mesh-controller.plan` cannot show an unmerged branch, so this is stated, not measured. restic runs on ace, g14, novox and shanks. On each:
- a new file, `<restic state>/retention.conf`, holding `daily=14`, `weekly=8`, `monthly=6`;
- the restic tools bundle is rebuilt and restarts with the new binary.
The nightly `restic forget` line is byte for byte the same (14/8/6), so no restore point is kept or dropped differently. No other file, unit or module changes.
Not touched: modules/mesh-delivery, modules/mesh-cli (open #198, #199).
The keep-daily, keep-weekly and keep-monthly preferences default to
today's 14, 8 and 6. The night reads them from a file in its state and
refuses anything but a count from 1 to 9999: it then thins nothing and
says which setting is wrong, never handing a raw value to restic.
Deliverynovox/mesh-catalog@ecf6d7803f8c — delivered since 2026-10-11T02:30:23Z
Delivery plan — builds restic → ace, g14, novox, shanks; no bus step
tier 0: restic
ace: receives restic
g14: receives restic
novox: receives restic
shanks: receives restic
restic keeps data (ADR 0233): what it holds is backed up before it moves
Machines
restic on ace: passed — healthy 3 times over 2m30s
restic on the rest running it: sent
Transitions
2026-10-11 01:10 (new) → proposed (announced): novox/mesh-catalog#200's head announced
2026-10-11 01:12 proposed → checked (checked): the verdict names this commit
2026-10-11 01:12 checked → ready (accepted): the gate passed or warned, and the repository's own check did not fail
2026-10-11 02:26 ready → published (merged): on the trunk its modules follow: merged there, and its walk opened — or nothing for a walk to move
2026-10-11 02:26 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-11 02:30 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-catalog@ecf6d7803f8c` — **delivered** since 2026-10-11T02:30:23Z
**Delivery plan** — builds restic → ace, g14, novox, shanks; no bus step
- tier 0: restic
- ace: receives restic
- g14: receives restic
- novox: receives restic
- shanks: receives restic
- restic keeps data (ADR 0233): what it holds is backed up before it moves
**Machines**
- restic on ace: passed — healthy 3 times over 2m30s
- restic on the rest running it: sent
**Transitions**
- 2026-10-11 01:10 (new) → proposed (announced): novox/mesh-catalog#200's head announced
- 2026-10-11 01:12 proposed → checked (checked): the verdict names this commit
- 2026-10-11 01:12 checked → ready (accepted): the gate passed or warned, and the repository's own check did not fail
- 2026-10-11 02:26 ready → published (merged): on the trunk its modules follow: merged there, and its walk opened — or nothing for a walk to move
- 2026-10-11 02:26 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-11 02:30 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/merge-gate: PASS — every machine composes with the change as it did without (4 of 4 compose) (modules: restic)
mesh/repo-check: PASS — its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
Delivery plan — builds restic → ace, g14, novox, shanks; no bus step
tier 0: restic
ace: receives restic
g14: receives restic
novox: receives restic
shanks: receives restic
restic keeps data (ADR 0233): what it holds is backed up before it moves
Run by the build seat on ace as build-1791680748004148785 (builds --log build-1791680748004148785).
--- the gate: restic
restic: ok, keeps repository (rebuildable), state (cache)
long-running resources without health: 0
files asking for a setting without saying whether it is trusted: 0
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-11T01:00:25Z 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-11T01:00:25Z
modules the change moves:
- restic (changed)
machines:
- a machine (f35): composes; the change adds 1 resource(s) and removes 0 (+file:restic.retention)
- holds mesh-dns-resolver (idt): composes; the change adds 1 resource(s) and removes 0 (+file:restic.retention)
- 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; the change adds 1 resource(s) and removes 0 (+file:restic.retention)
- a machine (yvbpsm): composes; the change adds 1 resource(s) and removes 0 (+file:restic.retention)
a merge would rebuild 1 module(s) in 1 tier(s): restic
--- its merge-check.sh (go toolchain)
ok git.novox.be/novox/mesh-catalog/checks/words 0.002s
words: 12 retired words and 70 daemons' lines, none in the 39460 strings of 655 files
ok git.novox.be/novox/mesh-catalog/checks/agenthome 0.011s
agent-home: no archive and no file written into under the agent's home
ok git.novox.be/novox/mesh-catalog/checks/paths 0.005s
paths: no module names another's state on its machine (3 known, listed to go)
long-running resources without health: 69, as kept
testing modules/restic
ok restic/cmd/restic-backups 1.044s
--- its merge-check-typescript.sh (typescript toolchain)
**Merge check** at `39d5b25f`
- `mesh/merge-gate`: **PASS** — every machine composes with the change as it did without (4 of 4 compose) (modules: restic)
- `mesh/repo-check`: **PASS** — its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
**Delivery plan** — builds restic → ace, g14, novox, shanks; no bus step
- tier 0: restic
- ace: receives restic
- g14: receives restic
- novox: receives restic
- shanks: receives restic
- restic keeps data (ADR 0233): what it holds is backed up before it moves
Run by the build seat on ace as `build-1791680748004148785` (`builds --log build-1791680748004148785`).
```
--- the gate: restic
restic: ok, keeps repository (rebuildable), state (cache)
long-running resources without health: 0
files asking for a setting without saying whether it is trusted: 0
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-11T01:00:25Z 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-11T01:00:25Z
modules the change moves:
- restic (changed)
machines:
- a machine (f35): composes; the change adds 1 resource(s) and removes 0 (+file:restic.retention)
- holds mesh-dns-resolver (idt): composes; the change adds 1 resource(s) and removes 0 (+file:restic.retention)
- 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; the change adds 1 resource(s) and removes 0 (+file:restic.retention)
- a machine (yvbpsm): composes; the change adds 1 resource(s) and removes 0 (+file:restic.retention)
a merge would rebuild 1 module(s) in 1 tier(s): restic
--- its merge-check.sh (go toolchain)
ok git.novox.be/novox/mesh-catalog/checks/words 0.002s
words: 12 retired words and 70 daemons' lines, none in the 39460 strings of 655 files
ok git.novox.be/novox/mesh-catalog/checks/agenthome 0.011s
agent-home: no archive and no file written into under the agent's home
ok git.novox.be/novox/mesh-catalog/checks/paths 0.005s
paths: no module names another's state on its machine (3 known, listed to go)
long-running resources without health: 69, as kept
testing modules/restic
ok restic/cmd/restic-backups 1.044s
--- its merge-check-typescript.sh (typescript toolchain)
```
mesh/merge-gate: PASS — every machine composes with the change as it did without (4 of 4 compose) (modules: restic)
mesh/repo-check: PASS — its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
Delivery plan — builds restic → ace, g14, novox, shanks; no bus step
tier 0: restic
ace: receives restic
g14: receives restic
novox: receives restic
shanks: receives restic
restic keeps data (ADR 0233): what it holds is backed up before it moves
Run by the build seat on shanks as build-1791681002021917746 (builds --log build-1791681002021917746).
--- the gate: restic
restic: ok, keeps repository (rebuildable), state (cache)
long-running resources without health: 0
files asking for a setting without saying whether it is trusted: 0
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-11T01:09:19Z 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-11T01:09:19Z
modules the change moves:
- restic (changed)
machines:
- a machine (f35): composes; the change adds 1 resource(s) and removes 0 (+file:restic.retention)
- holds mesh-dns-resolver (idt): composes; the change adds 1 resource(s) and removes 0 (+file:restic.retention)
- 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; the change adds 1 resource(s) and removes 0 (+file:restic.retention)
- a machine (yvbpsm): composes; the change adds 1 resource(s) and removes 0 (+file:restic.retention)
a merge would rebuild 1 module(s) in 1 tier(s): restic
--- its merge-check.sh (go toolchain)
ok git.novox.be/novox/mesh-catalog/checks/words 0.003s
words: 12 retired words and 70 daemons' lines, none in the 39460 strings of 655 files
ok git.novox.be/novox/mesh-catalog/checks/agenthome 0.013s
agent-home: no archive and no file written into under the agent's home
ok git.novox.be/novox/mesh-catalog/checks/paths 0.006s
paths: no module names another's state on its machine (3 known, listed to go)
long-running resources without health: 69, as kept
testing modules/restic
ok restic/cmd/restic-backups 1.088s
--- its merge-check-typescript.sh (typescript toolchain)
**Merge check** at `ecf6d780`
- `mesh/merge-gate`: **PASS** — every machine composes with the change as it did without (4 of 4 compose) (modules: restic)
- `mesh/repo-check`: **PASS** — its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
**Delivery plan** — builds restic → ace, g14, novox, shanks; no bus step
- tier 0: restic
- ace: receives restic
- g14: receives restic
- novox: receives restic
- shanks: receives restic
- restic keeps data (ADR 0233): what it holds is backed up before it moves
Run by the build seat on shanks as `build-1791681002021917746` (`builds --log build-1791681002021917746`).
```
--- the gate: restic
restic: ok, keeps repository (rebuildable), state (cache)
long-running resources without health: 0
files asking for a setting without saying whether it is trusted: 0
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-11T01:09:19Z 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-11T01:09:19Z
modules the change moves:
- restic (changed)
machines:
- a machine (f35): composes; the change adds 1 resource(s) and removes 0 (+file:restic.retention)
- holds mesh-dns-resolver (idt): composes; the change adds 1 resource(s) and removes 0 (+file:restic.retention)
- 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; the change adds 1 resource(s) and removes 0 (+file:restic.retention)
- a machine (yvbpsm): composes; the change adds 1 resource(s) and removes 0 (+file:restic.retention)
a merge would rebuild 1 module(s) in 1 tier(s): restic
--- its merge-check.sh (go toolchain)
ok git.novox.be/novox/mesh-catalog/checks/words 0.003s
words: 12 retired words and 70 daemons' lines, none in the 39460 strings of 655 files
ok git.novox.be/novox/mesh-catalog/checks/agenthome 0.013s
agent-home: no archive and no file written into under the agent's home
ok git.novox.be/novox/mesh-catalog/checks/paths 0.006s
paths: no module names another's state on its machine (3 known, listed to go)
long-running resources without health: 69, as kept
testing modules/restic
ok restic/cmd/restic-backups 1.088s
--- its merge-check-typescript.sh (typescript toolchain)
```
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.
Fixes the backup half of novox/hq issue 416 (backup retention and the container runtime's log rotation cannot become preferences yet). The log-rotation half needs a decision and is not in this pull request; see the note on 416.
Cause
The retention was a Go literal in
modules/restic/cmd/restic-backups/backups.go(Keep = {14, 8, 6}), so no setting could reach it. The controller's existing mechanism (ADR 0262 preferences filling${setting:}into a file, as zfs's scrub schedule does) supports it; no controller change is needed.Change
module.json: three preferences,keep-daily14,keep-weekly8,keep-monthly6 (today's values), and a new file${dir:state}/retention.conf(daily=,weekly=,monthly=) that asks for them. The file istrusted: true: lowering a count deletes restore points, so the value is set at the terminal alone (issue 339).retention.go: the night reads that file. Each value must be a count from 1 to 9999 with no leading zero; an unknown, repeated or missing key, or a line that is not key=value, is refused. A refused file means nothing is thinned, every restore point is kept, and the module says which setting is wrong in its journal. No raw value ever reaches restic's arguments (issue 346).Tests (written first)
retention_test.go: against main with stubs, three of the four fail (TestANightThinsByTheRetentionTheMeshWrote,TestARetentionThatIsNotACountThinsNothingAndSaysWhich,TestTheManifestsDefaultsAreTodaysRetention);TestWithoutTheFileANightKeepsTodaysRetentionpasses on main by design. The manifest test fills the file with the declared defaults and checks that it parses to exactly today's retention.Run here:
go vetandgo test -raceof modules/restic;merge-check.shwithMESH_GATEset to a controller built from mesh-controller main 11ffab8 andMESH_CHECK_CHANGEDset to this diff (exit 0,restic: ok, no untrusted-setting warning); the words check (cd checks/words && go run . ../.., clean).What changes on a machine with the defaults
mesh-controller.plancannot show an unmerged branch, so this is stated, not measured. restic runs on ace, g14, novox and shanks. On each:<restic state>/retention.conf, holdingdaily=14,weekly=8,monthly=6;The nightly
restic forgetline is byte for byte the same (14/8/6), so no restore point is kept or dropped differently. No other file, unit or module changes.Not touched: modules/mesh-delivery, modules/mesh-cli (open #198, #199).
Delivery
novox/mesh-catalog@ecf6d7803f8c— delivered since 2026-10-11T02:30:23ZDelivery plan — builds restic → 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
39d5b25fmesh/merge-gate: PASS — every machine composes with the change as it did without (4 of 4 compose) (modules: restic)mesh/repo-check: PASS — its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)Delivery plan — builds restic → ace, g14, novox, shanks; no bus step
Run by the build seat on ace as
build-1791680748004148785(builds --log build-1791680748004148785).Merge check at
ecf6d780mesh/merge-gate: PASS — every machine composes with the change as it did without (4 of 4 compose) (modules: restic)mesh/repo-check: PASS — its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)Delivery plan — builds restic → ace, g14, novox, shanks; no bus step
Run by the build seat on shanks as
build-1791681002021917746(builds --log build-1791681002021917746).