# Row 165 (#1662): the human adds and changes role instances Owner Darkwing. Reviewers Dewey (security) and Filbert. Gate owner Sage. Brief `docs/plans/2026-10-18_role-instance-verb.md`, first at blob 0aeb5f5adb66, corrected in b8036b608 to blob b3cf580a (rev 1873, mos-0823). Built on b8036b608, rebased onto 70c9131f0 before the slot run (sage-0939, sage-0941). Claimed 2026-10-18T21:25Z (rev 1871). ## Rebase onto 70c9131f0 (sage-0939) Row 154 landed as 9353675d1 and moved origin/refactor to 70c9131f0. Four of my manifest paths changed in b8036b608..70c9131f0: `docs/TOOLS.md`, `packages/webui/README.md`, `packages/webui/src/public/bus.js` and `packages/webui/src/serve.mjs`. I took the whole diff against b8036b608 and applied it with `git apply --3way` in a fresh worktree at 70c9131f0. All 16 changed files applied cleanly, and I copied the 7 new files over unchanged. The `public/` diff against the new base is `public-70c9131f0.diff`. Its +/- lines, hunk headers and context match `public.diff`. Only the `index` line differs (40c6b6758..b2bb52d0a, where it was 1e0be849d..67abd41f6). Sage ruled (sage-0941) that mos-0826 covers the +/- lines, so no new delta goes to Mos. To make the match checkable, each digest below is `grep '^[+-]' FILE | grep -vE '^(\+\+\+|---) ' | sha256sum`: | Diff | Whole-file sha256 | +/- lines | +/- digest | |---|---|---|---| | `public.diff` (on b8036b608) | `e5e3435b542fbbe2f69c64ac05ea200c02adc606a1c41020341620ddc19d13e0` | 55 | `04856a57c70cd6d96d33fd43aa285a17c0bb52a596c643145a52bf52c93db4e4` | | `public-70c9131f0.diff` (on 70c9131f0) | `ee57b150ca92664bb6a62bc9874d26340710a6ac2e5fc703eceba84505f48f96` | 55 | `04856a57c70cd6d96d33fd43aa285a17c0bb52a596c643145a52bf52c93db4e4` | On 70c9131f0, `human.mjs`, `human-cli.mjs`, `cli/src/transport.mjs`, `harness/src/tools.mjs`, `stack-release.sh` and `stack-copy.sh` are byte-equal to b8036b608 and to the candidate. The channel-proof pin `dfbc2289...9a4c` matches the rebased `webui/src/bus.mjs`. The 64-hex grep also hit `webui/tests/service.test.mjs` and `bus/tests/thread-migration.test.mjs`, but those strings are a fixture UID and a DDL hash, not pins on manifest files. Both tests run in singles. The 28-mutant dry run anchors on the new base. Stack scripts: this row doesn't touch `scripts/stack-release.sh`, `scripts/stack-copy.sh` or the 8e7ea89a1 re-pin target. If that changes, Sage hears before the review request (sage-0918). ## Paths beyond the brief's list (sage-0918) Sage approved the expansion at 21:30:34Z. The brief lists the writer, its test, the CLI command and the Console section. Everything below is beyond that list: - `packages/bus/src/broker.mjs`: the `instances` and `instances.set` verbs, human-only, with `INSTANCE_KEYS`. Neither is in `ORGANIZATION_KEYS`; - `packages/bus/src/runtime.mjs`: routes the two verbs to `instanceSettings`; - `packages/bus/tests/instances-verb.test.mjs` (new); - `packages/bus/tests/channel-proof.test.mjs`: the `webui/src/bus.mjs` pin, below; - `packages/harness/tests/tools.test.mjs`: proves no tool reaches the two verbs; - `packages/cli/src/cli.mjs`: routes `business instances` and `business instance` to the new `packages/cli/src/instances.mjs`; - `packages/webui/src/cli.mjs`: the wiring, under `--send` only; - `packages/webui/src/bus.mjs`: seven STATUS codes, below; - `packages/webui/src/serve.mjs`: `GET /api/bus/instances` and `POST /api/instances`; - `packages/webui/src/public/bus.js`: the Settings section (decision 123, below); - `packages/webui/tests/instances.test.mjs` and `instances-browser.test.mjs` (new); - `packages/webui/tests/shell.test.mjs`: its pinned list of bus.js POST fetches gets the `/api/instances` write, and the `method:` count goes to 6. I missed it in the reader grep. It reads bus.js's text, not a 64-hex pin. Slot run 1 caught it (see below); - `scripts/mosaic`: sends `business instances` and `business instance` to packages/cli; - `packages/bus/README.md`, `packages/business/README.md`, `packages/cli/README.md`, `packages/webui/README.md`; - `docs/TOOLS.md` and `docs/acceptance/314.md`, which the brief assigns. Untouched: `scripts/stack-release.sh`, `scripts/stack-copy.sh` and the 8e7ea89a1 re-pin target (`git diff --stat b8036b608` on the two scripts is empty). ## The seven STATUS codes and the channel-proof pin `packages/webui/src/bus.mjs` maps the writer's new codes to HTTP: | Code | Status | |---|---| | `organization-instance-exists` | 409 | | `organization-unknown-instance` | 400 | | `organization-credentials-out-of-scope` | 400 | | `organization-launch-not-configured` | 400 | | `organization-no-change` | 400 | | `organization-project-mismatch` | 400 | | `organization-unresolvable-instance` | 400 | `human-required` (403), `revision-conflict` (409), `organization-definition-refused` (400), `unknown-model` (400) and `invalid-request` were already there. The pin moves in its own commented hunk: ```diff - "packages/webui/src/bus.mjs": "d1f3a274f7aa15eb7ecc2267c65bf4fe7e45c898f77c34235b4e9c29cd1be241", // Row 155 (#1649): eight STATUS entries for the assistant routes, no proof or transport change + // Row 165 (#1662): seven STATUS entries for the role instance routes (organization-instance-exists, + // organization-unknown-instance, organization-credentials-out-of-scope, organization-launch-not-configured, + // organization-no-change, organization-project-mismatch, organization-unresolvable-instance), no proof or + // transport change. Row 155 (#1649) added eight for the assistant routes. + "packages/webui/src/bus.mjs": "dfbc2289337376b94a399a61599fd4af402b802deb2267dcffc5cf23502c9a4c", ``` The other three frozen files are byte-equal to b8036b608 (`git diff --quiet` exits 0 on each): `packages/bus/src/human.mjs`, `packages/bus/src/human-cli.mjs` and `packages/cli/src/transport.mjs`. `packages/harness/src/tools.mjs` is unchanged too. ## The public/ diff (decision 123) Nothing in `public/` is generated, so there is no separate handwritten-only diff. | File | sha256 | Lines | |---|---|---| | `public.diff` (`git diff b8036b608 -- packages/webui/src/public/`, bus.js +54/-1) | `e5e3435b542fbbe2f69c64ac05ea200c02adc606a1c41020341620ddc19d13e0` | 76 | | `public-delta-cb79be2e.diff` (the change from the first diff) | `ff2cbf6bc80a60c24fee374f2e14e9caa5c9619fd4d0a26ae722f54bb324adfe` | 42 | The first diff was `cb79be2eaa010e0974d64a38beafb5b8bc55a933e83dad7945cb7c0c4aaa5c8d` (74 lines). Mos gave a conditional go on it (mos-0825) with one finding to fix and one note: - F1: the page took a 200 answer as its next view after checking only the revision and `restart`, so an answer without `definitions` would throw in the next render. The page now checks the whole shape (`instanceAnswer`: a 64-hex revision, a boolean `restart`, and `instances` and `definitions` as non-array objects). A 200 that fails it keeps the view and the draft and says "Outcome unknown. Read the role instances again before trying. Draft kept." `serve.mjs` makes the same check on the bus answer and refuses with 503 `outcome-unknown`, `sent: "unknown"`. Sage asked for both (sage-0931): the page doesn't trust the server's shape. - F2: the restart text named a seat. Sage folded it in (sage-0931). The page now says "Restart needed: the running bus keeps the roles it loaded at start. The Console does not restart it." and the CLI says "restart needed: the running bus keeps the roles it loaded at start; this command does not restart it". Mos's go on e5e3435b is mos-0826. It replaces the conditional go. It holds while the slot run is green (28 mutants included), the reviewers cover the writer's checks and the new 503 on a bad answer shape, and the +/- lines are unchanged at landing. Any later `public/` change goes back to Mos as a delta against e5e3435b. ### The warn line and mos-0832 (current go) Slot run 1 on 70c9131f0 found that `views.test.mjs` waits for Settings' notifier empty state as `#bus-view .empty b`. When the instances read failed, my section drew `empty()` above Notifications, so the wait matched my box. On this page `empty()` is for a view with nothing in it. A section whose read failed shows a `
` line (bus.js 268 and 284). Line 683 now follows that convention: ``` - if(!view)return `...${title}${empty('The role instances could not be read.','They are read from the bus host with your identity.')}`; + if(!view)return `...${title}
The role instances could not be read, so there is nothing to add or change. Read again to retry; they come from the bus host with your identity.
`; ``` `instances-browser.test.mjs` reloads Settings with the read failing. It checks the exact warn text, and that the section has no `.empty` box and no instance form. Mutant G5 puts the old line back. | File | sha256 | Lines | +/- digest | |---|---|---|---| | `public-70c9131f0-warn.diff` (the candidate's whole `public/` diff, bus.js +54/-1) | `4181b7f649346aa9bb166cd5597e6474fa7011e5f1f9adb1780c0586c648f7a7` | 76 | `939ecd28f80bc2f4a36a9468be0ccc2b0f08c8a630508060572491357f1f9444` | | `public-delta-e5e3435b.diff` (the one-line change) | `fa1012afba5651abaa50b38a304696469a797db485ff0383bf16df5a6f5cd176` | 11 | | Mos gave the decision 123 go on 4181b7f6 in mos-0832. It replaces the go on e5e3435b, and the mos-0826 conditions still apply: 1. a green slot run, with the singles and all 29 mutants recorded here; 2. reviewer approvals that cover the server-side writer and the 503 `outcome-unknown`; 3. the +/- digest is still 939ecd28 at landing. Any later `public/` change goes back to Mos as a delta against 4181b7f6. `ORGANIZATION_KEYS` already has an `instances` key. It is row 125's model selection, which sets `vars.model` on an existing instance and nothing else. The new verb shares the word and nothing more: its args use the singular `instance`. ## The credential refusal (mos-0823) The code is `organization-credentials-out-of-scope`. In the business vocabulary a role's credentials are what `credentials` binds, and binding them stays a hand edit in this row. The `organization-` prefix lets the broker pass the code through unchanged (`broker.mjs`, the organization branch's error mapping), so the CLI and the Console show it by name. The writer has three behaviors here, each with its own test and its own mutant: 1. An add whose definition declares credentials is refused, and the message names the services: `reviewer declares gitea and vikunja credentials; binding them is a hand edit`. The same refusal covers a change of definition to such a definition. The file's bytes don't change. 2. The writer never writes an instance without its credential binding. A change away from a definition that declares credentials is refused too, with `: . Nothing changed.` and the page adds
"Draft kept.". Mutants W6 and L2 drop the lookups.
## Tests that read a manifest path (sage-0918 point 1)
I grepped every `packages/*/tests/*.mjs`, `scripts/test-*.sh` and `scripts/foundation/*.test.mjs`.
For each manifest path the grep looked for the repo path, a `../..//` import and a
`../` import. Hits, by path:
| Path | Test files |
|---|---|
| `packages/bus/src/broker.mjs` | 60 (bus, cli, harness, seat, tasks via `world.mjs`, webui) |
| `packages/bus/src/runtime.mjs` | 16 |
| `packages/cli/src/cli.mjs` | 27 |
| `packages/cli/src/instances.mjs` | 1, `cli/tests/instances.test.mjs` |
| `packages/business/src/instance-settings.mjs` | 1, `business/tests/instance-settings.test.mjs` |
| `packages/webui/src/bus.mjs` | 12, among them the `channel-proof.test.mjs` pin, the only 64-hex pin on a manifest path |
| `packages/webui/src/cli.mjs` | 29, among them `cli/tests/stack-copy.test.mjs` and `stack-release.test.mjs` |
| `packages/webui/src/serve.mjs` | 33 |
| `packages/webui/src/public/bus.js` | 0 by repo path. The browser tests load it through the server, and `webui/tests/shell.test.mjs` reads it as `read('bus.js')`, which the path grep missed |
| `scripts/mosaic` | 16, among them `scripts/test-config.sh`, `test-queue.sh` and `test-task.sh` |
| `packages/cli/README.md` | `cli/tests/host.test.mjs` (the unit file's name must appear) |
| `packages/webui/README.md` | `webui/tests/bus-browser.test.mjs` (row 61's refused field table wording) |
| `packages/bus/README.md`, `packages/business/README.md`, `docs/TOOLS.md`, `docs/acceptance/314.md` | none |
The README hits correct my earlier note that no test reads the READMEs: findings on the cli and
webui READMEs aren't doc-only. A finding on the bus or business README, TOOLS.md or acceptance 314
is doc-only (decisions 136/137).
Some hits came from a package's own `../src/serve.mjs` or `../src/cli.mjs` (control-board,
discord) or from `.mosaic` in `scripts/foundation/cli.test.mjs`. I read each one, and none reads a
manifest path. Every real reader is in `singles.sh`: each `*.test.mjs` in bus, business, cli and
webui (143 files), harness `runner` and `tools`, seat `sandbox` and `session`, the eight tasks tests
that import `world.mjs`, queue `dispatch`, runs `task-cli`, then `scripts/test-config.sh`,
`test-queue.sh` and `test-task.sh`. Each file runs singly from its package directory.
## Mutants
There are 29, each paired with the tests that should kill it. The second column says what the
mutant changes:
| Id | Change | Tests | Result |
|---|---|---|---|
| K1 | the broker's `ORGANIZATION_KEYS` gets the instance keys | bus, harness tools | killed |
| K2 | all three key lists get them | bus, harness tools | killed |
| K3 | `change_organization` sends `instances.set` | harness tools | killed |
| H1 | the human-required check skips the `instances` verbs | bus | killed |
| H2 | the broker's `INSTANCE_KEYS` gets `holder` | bus | killed |
| C1 | the add credentials check is off (mos-0823 behavior 1) | business, bus, cli, webui | killed |
| C2 | the old-definition credentials check is off (behavior 2) | business, bus, cli, webui | killed |
| C3 | a vars change drops the binding (behavior 3) | business, cli | killed |
| P1 | `definitionFor` skips `END_PROTECTED` | business, bus | killed |
| P2 | a protected instance can change | business | killed |
| V1 | no validation of the next document | business | killed |
| V2 | no `no-change` check | business | killed |
| R1 | the write takes the current revision, not the caller's | business, bus, cli, webui | killed |
| A1 | the actor is `system` | business, bus, cli, webui | killed |
| S1 | `restart` is always false | business, bus, cli, webui | killed |
| W1 | the route ignores `--send` | webui | killed |
| W2 | no Origin check | webui | killed |
| W3 | no Sec-Fetch-Site check | webui | killed |
| W4 | the Console's `INSTANCE_KEYS` gets `holder` | webui | killed |
| W5 | the body limit is 64 KiB | webui | killed |
| W6 | the Console drops the reason | webui | killed |
| W7 | the Console passes an answer without `definitions` (F1) | webui | killed |
| G1 | the page never names a restart | page | killed |
| G2 | a refusal drops the draft | page | killed |
| G3 | the page offers definitions that declare credentials | page | killed |
| G4 | the page takes a 200 without `definitions` as its view (F1) | page | killed |
| G5 | an unread instances view draws the page's empty state, not a section warning | page | killed |
| L1 | the CLI never names a restart | cli | killed |
| L2 | the CLI drops the reason | cli | killed |
`DRY=1` finds every anchor exactly once (`DRY 29 mutants ok`). The results above are from mutants
run 2 on wt2, `out/mutants-20261018T224704Z`: 29 killed, none survived.
## Slot run (sage-0939)
Sage gave the slot at 22:18:52Z after Dewey cleared. Everything ran in
`~/darkwing-scratch/r165/wt2` at 70c9131f0 plus the candidate, with `COMPOSE_PROJECT_NAME=darkwing-r165`,
pipefail, and the counts summed with awk over the logs.
| Run | Output dir under `~/darkwing-scratch/r165/out/` | Result |
|---|---|---|
| Singles run 1 | `singles-20261018T222149Z` | pass 1417, fail 2 |
| Singles run 2 | `singles-20261018T223459Z` | pass 1419, fail 0 over 163 logs, overall green |
| Mutants run 1 | `mutants-20261018T224446Z` | 28 of 29 killed, W1 survived |
| W1 alone | `mutants-20261018T224658Z` | killed |
| Mutants run 2 | `mutants-20261018T224704Z` | 29 of 29 killed |
Singles run 2 includes `scripts/test-config.sh`, `test-queue.sh` and `test-task.sh`, which passed
24, 28 and 98.
Singles run 1 had two real failures, neither a flake or board-held:
- `shell.test.mjs` pins bus.js's POST fetches, and the new `/api/instances` write wasn't in its
list. Fixed in the test (above).
- `views.test.mjs` matched `#bus-view .empty b` against my instances empty state. The page now
uses the section-warning convention, which is the `public/` change Mos approved in mos-0832.
The new last step in `instances-browser.test.mjs` and mutant G5 cover it.
W1 survived mutants run 1 because of a test gap. In `webui/tests/instances.test.mjs`, the
send-off server also had no writer, so a route that skipped `!send` still refused on
`!instancesWrite`. The test now starts a third server with a writer and send off, and expects 403
and no write. That file passed singly (5 pass, 0 fail, `out/fix3-webui-instances.log`), W1 alone
was killed, and mutants run 2 killed all 29. The change is test-only, so the `public/` +/- digest
is still `939ecd28...9444` (mos-0832 condition 3).
After the runs, the /proc sweep found 0 of my processes, and the `darkwing-r165_default` network
is gone with 0 containers.
## Round 2 (Dewey 29037, Filbert 29036)
Filbert approved round 1 with three notes. Dewey asked for changes on F1 and F2. Round 2 changes
7 files against the round 1 candidate and adds none. `webui/src/bus.mjs` is unchanged, so the
channel-proof pin stays put. The `public/` +/- digest is still `939ecd28...9444`, so mos-0832
still covers the page.
**Dewey F1.** The writer took `human` and `tester` as instance ids. Both match ID_PATTERN, and
`validateBusinessDocument` only runs `requireId`. The broker refuses a business whose roles use
either name when it loads (`broker.mjs`, pinned by `bus/tests/tester.test.mjs`), so an add said
"Restart needed" and the next bus start failed closed. `instance-settings.mjs` now refuses both
names with `invalid-request`, before it reads the file. The business test covers each name, and
mutant F1 removes the check. I chose `invalid-request` over a new code because a new code needs
REASONS, INSTANCE_REASONS and a STATUS entry, and that moves the channel-proof pin. The cost is
that the human sees `invalid-request` without the writer's message, because the broker drops it
(Dewey N3, Filbert note 1). I left the rule out of `validateBusinessDocument`, since that would
change what every business file accepts. That's a follow-up for Sage to place.
**Dewey F2.** The route's 503 shape check had a test only for `definitions`. The table in
`webui/tests/instances.test.mjs` now has three more cases, each wrong in one part:
`revision: 'x'`, `restart: 'yes'`, and `instances: {}`. Mutants F2a, F2b and F2c each drop one
check. I also took the two optional GET cases. An answer without `instances` or without
`definitions` gets 502, and mutants E1 and E2 drop each check. These are test changes only.
**Filbert note 2.** The `organization-definition-refused` reason said "an assistant instance
changes only through ...", which reads wrong for an add or change that names the assistant
definition. Both tables now say the command or form "neither adds nor changes an assistant, and
moves no instance to the assistant definition". The CLI test and a real-broker case in the
webui route test pin the new wording. Mutants N2a and N2b put the old wording back.
**Filbert note 3.** The route test already has a revision-conflict case through a real broker,
at `webui/tests/instances.test.mjs:90`. Nothing changed for it.
**Follow-ups, not in this row.** Dewey N1: check a stale revision before anything else. Dewey
N2: a new instance key can take the name of a held seat. Dewey N3 and Filbert note 1: the broker
drops the writer's message, so a refusal reaches the human as its code alone.
The business README has a new bullet under "Role instances" for the `human`/`tester` refusal.
### Round 2 slot run (sage-0950)
`~/darkwing-scratch/r165/wt2` at 70c9131f0 plus the round 2 candidate, with
`COMPOSE_PROJECT_NAME=darkwing-r165`.
| Run | Result |
|---|---|
| singles, 163 files, 23:12:51 to 23:22:25 | every rc 0; awk sum over the 163 logs pass 1419, fail 0 |
| `mutants.py`, 37 mutants | 37 killed, none survived |
Logs: `~/darkwing-scratch/r165/out/singles-20261018T231251Z` and
`~/darkwing-scratch/r165/out/mutants-20261018T232237Z`. After the mutants wt2 matched the candidate
manifest, the sweep found no process and no darkwing network was left.
Dewey F2's three 503 checks, all in `serve.mjs:314`, each with its own mutant that
`webui/tests/instances.test.mjs` kills (rc 1, 1 fail):
| Mutant | Check it removes | Case that kills it |
|---|---|---|
| F2a | `!/^[a-f0-9]{64}$/.test(result?.revision ?? '')` | `revision: 'x'` |
| F2b | `typeof result.restart !== 'boolean'` | `restart: 'yes'` |
| F2c | `!result.instances[instance]` | `instances: {}` |
| E1 | GET 502, `serve.mjs:91`: `!value.instances \|\| typeof value.instances !== 'object'` | an answer without `instances` |
| E2 | GET 502, `serve.mjs:91`: `!value.definitions \|\| typeof value.definitions !== 'object'` | an answer without `definitions` |
F1 (the human/tester refusal) dies in `instance-settings.test.mjs`. N2a and N2b (the old wording in
the CLI and the Console) die in `instances.test.mjs`.
## Round 3: rebase onto 4f45cadc0 (sage-0957, sage-0958)
Row 156 landed as 4f45cadc0, and its paths include 4 of this row's 24: `docs/TOOLS.md`,
`packages/bus/src/broker.mjs`, `packages/bus/src/runtime.mjs` and `packages/cli/README.md`. The
round 3 worktree is `~/darkwing-scratch/r165/wt3`, a fresh detached worktree at 4f45cadc0 with
round 2's `build.patch` (`cc886c47...`) applied. `git apply --check` was clean and `git apply`
needed no fuzz or 3-way merge. No round 2 line changes.
Per path, against round 2:
| Path | Result |
|---|---|
| the 20 paths row 156 doesn't touch | byte-equal to wt2 |
| `docs/TOOLS.md` | the file differs by row 156's lines. This row's +/- lines are identical to round 2's (2 hunks, digest prefix `b74e6dd8`) |
| `packages/bus/src/broker.mjs` | the same, 2 hunks, `446ffd46` |
| `packages/bus/src/runtime.mjs` | the same, 3 hunks, `3bb624f8` |
| `packages/cli/README.md` | the same, 1 hunk, `bf46a856` |
The digest prefix is the sha256 of the file's `git diff BASE -- FILE` +/- lines, without the
`+++` and `---` headers, with BASE 70c9131f0 for round 2 and 4f45cadc0 for round 3.
The `public/` diff against 4f45cadc0 has +/- lines byte-equal to round 2's, and their digest is
still `939ecd28f80bc2f4a36a9468be0ccc2b0f08c8a630508060572491357f1f9444`, so mos-0832's go
covers it unchanged. Row 156 doesn't touch `public/`.
What row 156 changes in the shared code files, and why it doesn't meet this row:
- `broker.mjs`: 156 adds `#received` (set in `message.receive`), `landHolders`, `landRelease`, the
`#held` generator behind `landHeld`, and a landing-turn case in `#requestUsage`. This row adds
`INSTANCE_KEYS` and the `instances` and `instances.set` cases in `Broker.request`'s organization
branch. No function has lines from both rows.
- `runtime.mjs`: 156 adds `landHolders` and `landRelease` to the launch API. This row adds the
`instanceSettings` import and writer, and the two verb lines in the organization callback. Both
rows' lines are inside `startBroker` (lines 42 to 223 in wt3), in separate statements: this
row's at 70-71 and 99-100, 156's at 202-204 in the launch API object.
Correction (round 3, Filbert 29053): the sentence "No function has lines from both rows" holds
for `broker.mjs` only. In that file 156 doesn't touch `request()`; its lines are the `#received`
field, `landHeld`, `landHolders`, `landRelease`, three comment lines between `request()` and
`#requestUsage`, and `#requestUsage` itself. My review requests to Dewey and Filbert applied the
sentence to all four shared paths, which was wrong for `runtime.mjs`. The statements don't share a
line or a variable that either row changes, so the result above stands.
- 156's other code paths are `bus/src/process.mjs`, `cli/src/launcher.mjs` and
`harness/src/runner.mjs`. No file of this row imports them, and they don't import this row's
new modules.
One reading I checked: `landHolders` skips a role that `#threaded` reports as a thread instance,
and an instance this row adds is unknown to the broker until a bus restart, as "How a change
reaches a running bus" says. So an added instance can't hold a merge decision before the restart.
The CLI's and Console's "no restart" receipt rests on the launcher reloading the business file at
each launch check. After row 156 it still does, at `launcher.mjs:227` (198 at 70c9131f0).
Round 3 build, from a temp index (`git read-tree 4f45cadc0`, `git add` the 24 paths,
`git diff --cached --binary 4f45cadc0`):
| File | sha256 |
|---|---|
| manifest of the 24 files | `e30b54da42d5552b7320a458a5f1c808ac2c5ae6027f554ddc01e3ffc844f580` |
| `build.patch` against 4f45cadc0 | `cefce7a49c3bbe7863a68449a5eb7f74af0cefbbb5d1d8fb55c3be416d5e08aa` |
### Round 3 slot run (sage-0959)
On wt3, `TMPDIR=~/darkwing-scratch/r165/tmp`, `COMPOSE_PROJECT_NAME=darkwing-r165`:
| Run | Result |
|---|---|
| `singles.sh` with `WT=wt3`: 160 test files singly, then `test-config.sh`, `test-queue.sh` and `test-task.sh` | every one of the 163 exits 0. The awk sum over the logs is pass 1436, fail 0 (round 2 was 1419). The 17 more are row 156's: `land` 11 to 17, `launcher` 19 to 24, `thread-launch` 10 to 11 and `runner` 27 to 32. Every other file's count is unchanged |
| `mutants.py` with `WT=wt3`, all 37 | 37 killed, none survived |
Logs: `~/darkwing-scratch/r165/out/singles-20261018T234323Z` and
`~/darkwing-scratch/r165/out/mutants-20261018T235312Z`. wt3 matched the manifest after both runs,
the sweep found no process and no `darkwing-r165` network was left. `test-queue.sh` skips its
`verify` and `render --check` cases outside the canonical checkout, as in round 2.
## The candidate
The candidate is wt3: 4f45cadc0 plus 24 files, 17 changed and 7 new. Round 3 is round 2 rebased
onto row 156's landing commit, with no line of this row changed (see "Round 3").
| File | sha256 |
|---|---|
| `candidate-manifest.sha256` (the 24 files) | `e30b54da42d5552b7320a458a5f1c808ac2c5ae6027f554ddc01e3ffc844f580` |
| `build.patch` (the whole diff against 4f45cadc0) | `cefce7a49c3bbe7863a68449a5eb7f74af0cefbbb5d1d8fb55c3be416d5e08aa` |
Round 2 was manifest `4bcd6a2e...` and build.patch `cc886c47...` against 70c9131f0; the packet
keeps them as `candidate-manifest-r2.sha256` and `build-r2.patch`. Round 1 was `89ca3fcf...` and
`d84406b8...`. The `public/` +/- digest is still `939ecd28...9444`, the one mos-0832's go names.
The commit message is `commit-msg.txt`, unchanged from round 2.
## Proposal, before any code (brief item 2)
Line numbers are at d88613bac.
### The command
`scripts/mosaic` sends `business instances` and `business instance` to `packages/cli/src/cli.mjs`.
`validate` and `resolve` still go to `packages/business/src/cli.mjs`. The human commands live in
packages/cli because that is where the human proof and the other-tree check
(`packages/cli/src/cli.mjs:581-586`) already are.
```
mosaic business instances [--json]
mosaic business instance add --definition DEF [--vars FILE] [--launch] --revision REV
mosaic business instance set [--definition DEF] [--vars FILE] [--launch | --no-launch] --revision REV
```
- `instances` prints the revision and, for each instance, its definition, its vars, whether it is
in `launch.instances`, and whether it is `launch.by` or protected. It also lists the definitions
in `roles/` that `add` and `set` accept.
- `--vars FILE` is a JSON object that replaces the instance's `vars` whole, the way the
`assistants` key does in row 155.
- `--revision` is required. A stale one is refused with `revision-conflict` (exit 2).
- A write prints the business, the new revision, the transaction id, and whether the change waits
for a bus restart (below).
- The business is positional, the way it is for `validate` and `resolve`.
### The bus verbs
There are two new human-only verbs, `instances` and `instances.set`. They go into the
organization branch of `Broker.request` (`packages/bus/src/broker.mjs:1157`), next to `plugins`
and `plugins.set`, and `fail("human-required")` covers them the same way. They aren't added to
`ORGANIZATION_KEYS`, and `organization.set` never maps to them. That keeps them out of the chief
of staff's tool, because `packages/harness/tests/tools.test.mjs` holds the tool's keys equal to
`SETTINGS_KEYS`, and those stay as they are.
`instances.set` args:
`{ revision, instance, op: "add" | "change", definition?, vars?, launch?: boolean }`.
`add` needs `definition`. `change` needs at least one of `definition`, `vars` and `launch`.
### The writer
There is one new module, `packages/business/src/instance-settings.mjs`, which is
`instanceSettings({ dir, rolesDir, system })` with `read(id)` and `set(id, args, actor)`, built
on `organizationWriter().write`. That follows `plugin-settings.mjs`. In order, it:
1. checks the shape: the instance id goes through `requireId`. `add` refuses an existing id with
`organization-instance-exists`, and `change` refuses a missing one with
`organization-unknown-instance`;
2. refuses with `organization-definition-refused` when the instance's old or new definition is in
`END_PROTECTED`. Every change to a protected instance, its vars and launch membership
included, is refused here, because the assistant's settings go only through row 155's
`assistants` key. This also covers adding an instance with a protected definition;
3. builds the next document. `launch: true` adds the instance to `launch.instances`, and `false`
removes it. With no `launch` block it is refused with `organization-launch-not-configured`;
4. runs `validateBusinessDocument`, then the unknown-model check from
`launch-settings.mjs:159-165`, then the project check `mosaic business validate` runs
(`packages/business/src/cli.mjs:290-301`). For each project whose project file exists, it
runs `loadProject`, checks the id, and runs `resolveInstance` for every instance;
5. writes through `writer.write(id, next, { revision, actor })`. That entry records the actor, the
before and after hashes and the changes in `changes.jsonl`.
`validate` also stat-checks credential references. This writer can't add or change one, because
`credentials`, `holder` and `tracker` are out of scope. A change of definition that needs
different services fails `checkCredentialMap`, so the writer doesn't re-stat references.
### The Console
The Settings page gets a "Role instances" section. It has a table from `GET /api/instances` and
two forms, add and change, that `POST /api/instances`. The routes call the bus through
`webui/src/cli.mjs` under `--send` only, the way `settingsWrite` does. The form posts the revision
it read. Mos's decision 123 go covers this `public/` diff.
### How a change reaches a running bus (brief item 6)
- The launcher reloads the business file at every launch check (`launcher.mjs:198`), so launch
membership and vars changes count at the next launch.
- The broker keeps the business it loaded at bus start. `bindLaunch` refuses a role it doesn't
know with `unknown-role` (`broker.mjs:414`), so an added instance can't launch until the bus
restarts.
- A definition change reaches the broker's authority checks only after a restart. Until then the
launcher would start the new contract while the broker checks the old definition's authority.
So the writer's answer carries `restart: true` for an add or a definition change. The CLI and
the Console say so, and the restart is Jason's or Mos's call.
## Questions for Sage (answered)
Sage answered all three in sage-0918: the expansion is approved, the build keeps the brief's
scope with a named refusal for credentials, and the restart is reported, not run. The questions
as sent, in the order they blocked code:
1. **Files beyond the brief's list.** The verbs need these:
- `packages/bus/src/broker.mjs` and `runtime.mjs`, for the two verbs;
- a new `packages/bus/tests/instances-verb.test.mjs`;
- `packages/bus/README.md`;
- `packages/webui/src/cli.mjs`, for the wiring;
- `packages/webui/src/bus.mjs`, which gets two STATUS codes, so the
`channel-proof.test.mjs` pin needs a commented bump, as in row 155 r3;
- `scripts/mosaic`, for the routing.
I can't build the verbs without them. Do you approve the expansion?
2. **A second reviewer can't be added in this row.** `reviewer`, `coder`, `pm` and `cto` declare
gitea and vikunja credentials, so an instance of any of them needs `credentials` and `tracker`,
which the brief keeps out of scope. `security-reviewer` and `merge-gate` declare none, so
acceptance 314's two instances do work. I'd keep the brief's scope and record a follow-up for
credentials and tracker, but Q171's example was "a second reviewer", and that reading is
Jason's.
3. **The definition mismatch until a bus restart** (above). Within this row I'd only report it. A
launcher refusal on a mismatch would be a separate bus row.
## Unrelated finding
`mosaic plugins` (row 145, 467a313c7) isn't in the case list at `scripts/mosaic:33`, so it looks
like it falls through to the seat CLI. I haven't run it. This is a follow-up, not part of this row.