Files
jason.woltje 158205c5cc lead: commit darkwing's untracked evidence (348 files: queue-137 queue-141 queue-150 queue-162 queue-165 queue-166 queue-167 queue-169 queue-179 queue-180 queue-181 queue-182 queue-185 queue-186 queue-187 queue-188 queue-190 queue-93)
Unclaimed files left in the canonical checkout. Inventory: canonical-cleanup-inventory.md (commit-as-evidence groups). detect-secrets scan clean on 2026-11-02: hits are sha256 pins, git object ids, test sentinels already in tracked tests and unit-file lines.
2026-11-02 15:05:14 -06:00

35 KiB

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 0aeb5f5adb, 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:

- "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 <p class="small warn"> 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.')}</section>`;
+    if(!view)return `...${title}<p class="small warn">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.</p></section>`;

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 <instance> holds <definition> credentials; moving them to another definition is a hand edit. checkCredentialMap would refuse most of these anyway. The writer's own check names the reason instead of a validation message.
  3. A vars or launch change on an instance that holds its binding succeeds, and credentials and tracker come back unchanged.

The other codes, all with the organization- prefix:

Code When
organization-instance-exists add names an instance already in roles
organization-unknown-instance change names an instance not in roles
organization-definition-refused the old or new definition is in END_PROTECTED, or the instance is protected
organization-launch-not-configured launch given and the file has no launch block
organization-no-change the result is the same document
organization-project-mismatch a project file's id differs from its key
organization-unresolvable-instance resolveInstance refuses an instance for a project

A stale revision is revision-conflict. A bad shape and a document validate would refuse are invalid-request, and an instance whose model isn't in launch.models is unknown-model.

The restart report (sage-0918 point 3)

The writer's answer carries restart: true after an add or a definition change. The CLI prints restart needed: the running bus keeps the roles it loaded at start; this command does not restart it. The Console receipt says "Restart needed: the running bus keeps the roles it loaded at start. The Console does not restart it." Product text names no seat (sage-0931). A vars or launch change prints no restart: the launcher reads the change at its next launch check, and the page says the same. Neither surface offers to run a restart, and the tests assert that: the CLI test checks the exact string and that it doesn't match Mos or Jason, and the page test checks there is no button that mentions a restart.

The restart itself needs Mos's go or Jason's hand. TOOLS.md, acceptance 314 and the bus and cli READMEs say so as policy. No test reads TOOLS.md, acceptance 314 or the bus README.

The REASONS tables

The CLI (packages/cli/src/instances.mjs, REASONS) and the Console (serve.mjs, INSTANCE_REASONS) each keep a reason per code, worded for their own surface: the CLI names mosaic business instances and the Console names the read. A code without a reason prints refused by the bus. A refusal is <code>: <reason>. 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 ../../<pkg>/<path> import and a ../<path> 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 <business> [--json]
mosaic business instance add <business> <instance> --definition DEF [--vars FILE] [--launch] --revision REV
mosaic business instance set <business> <instance> [--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.