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

33 KiB
Raw Permalink Blame History

Row 190: several Mosaic Stack instances on one host

Brief: docs/plans/2026-10-20_multi-instance.md, blob 4e8f1e052894 (commit 48ebee0ff, rev 2259). Gitea #1688. Owner Darkwing, reviewer Filbert, gate owner Sage. Design: DESIGN.md in this directory, revision 3 (sha256 75b21f7561cb...), approved by Filbert. Row 191 (Rocko) merge-chains on this candidate.

Round 1 is the record below up to "Follow-ups". Filbert's round 1 verdict was CHANGES (Gitea comment 29634, queue rev 2362), and "Round 2" at the end replaces round 1's Candidate and gate sections.

Files

File What
DESIGN.md the approved design, revision 3
build.patch the change, git diff da230ca48 over the 24 paths below, the two new files included
candidate-manifest.sha256 one line per path
ti8/ the T-I8 one-shot: the test file (not committed), its runner, record.txt and the run logs

Base: da230ca48. Changed (22) and new (2):

  • Scripts: scripts/instance.sh (new), scripts/mosaic-config.mjs, scripts/bootstrap.sh, scripts/bus-service.sh, scripts/webui-service.sh, scripts/discord-service.sh, scripts/stack-copy.sh, scripts/stack-release.sh.
  • Templates: packages/cli/systemd/mosaic-bus.service.in, packages/webui/systemd/mosaic-webui.service.in, packages/discord/systemd/[email protected]. Each gains one @CONFIG_ENV@ line, and the bus template's EnvironmentFile becomes @SESSION_ENV@.
  • Guard: packages/conversation/src/guard.mjs.
  • Tests: packages/cli/tests/stack-instances.test.mjs (new), packages/cli/tests/stack-release.test.mjs (T55 and the stand-in), and fixture changes in packages/cli/tests/host.test.mjs, packages/webui/tests/service.test.mjs, packages/webui/tests/pinned-copy.test.mjs, packages/discord/tests/copy-service.test.mjs and scripts/test-discord.sh.
  • Docs: AGENTS.md (the two lines in Mos's wording, sage-1335), docs/TOOLS.md (the --instance section, which I wrote, per sage-1335), and the three package READMEs.

The worktree's node_modules symlink is not part of the candidate.

The tests

Design test Where Subtests
T-I1 bootstrap stack-instances.test.mjs 7: alpha and beta created with modes; a second run changes no byte, mode or mtime; a non-empty unmarked root refuses, worktrees included; an empty root is claimed and a lost marker refuses; a dangling symlink at the config path is never written through, named or default; bad names exit 4; --instance dev writes what no flag writes
T-I2 selection stack-instances.test.mjs 6: the four D1 rows, a named instance with no config, and the flag through an entry point
T-I3 render T55 in stack-release.test.mjs each instance's own MOSAIC_CONFIG, session env and Description, nothing of the other's
T-I4 ports T55 port-claimed for the Console port and the tester port, the default on a named port, the reserved ports, the stack-release run and --plan
T-I5 side by side stack-instances.test.mjs two real bus hosts at once, each answering from its own socket; stopping alpha leaves beta up
T-I6 no cross-reach T55 alpha's activate and rollback reach nothing of beta's, then swapped
T-I7 claims stack-instances.test.mjs (4) and T55 (1) the mismatch in every op, D1 row 2, a default config on alpha's root, and each bad marker shape; T55 runs D1 row 2 through stack-release.sh activate
T-I8 default unchanged ti8/ one-shot, plus the existing suites see below
T-I9 busy scoping T55 another instance's bus refuses nothing; this one's does
T-I10 guard stack-instances.test.mjs every instance's config dir and roots, a corrupt config, mosaic.bak

T-I8

The one-shot copies the fixture repository, puts the da230ca48 blob of each of the 11 changed scripts, templates and guard into the copy, removes scripts/instance.sh from it, and asserts each of the 11 now differs from the candidate's. It then runs the same 15 commands from both trees on one seeded fixture:

  • bus-service.sh render with and without --pm, webui-service.sh render with and without --tester-port, discord-service.sh render, and stack-copy.sh check with and without --business;
  • stack-release.sh status, four activate ... --plan forms (C1, C1 with --tester-port, C with --migrate, and B, which refuses) and rollback --plan (which refuses);
  • after a real activate C1, status and rollback --plan again.

Exit code, stdout and stderr match on all 15 (ti8/record.txt holds each sha256). The one normalization replaces the base tree's path with the candidate's in the output, since each script names its own repository. discord-service.sh render runs without --copy: a copy render needs packages/discord/src/cli.mjs, which the fixture copy lacks (run 2 failed on that, ti8/run-2.log; run 3 passed, ti8/run-3.log). The checkout render goes through the same template fill. The test file is ti8/zz-ti8-oneshot.test.mjs.txt (sha256 e888e55e140a...). It is not in the candidate, because it reads da230ca48 from git and stops meaning anything once that base is old.

The default renders are also pinned by the existing fixtures: service.test.mjs and copy-service.test.mjs compare a full render with the template, now with the @CONFIG_ENV@ line deleted.

Deviations and readings

  1. Names alpha and beta, not the design's a and b. The D1 rule ^[a-z][a-z0-9]{1,15}$ needs two characters or more, so a is invalid.
  2. The new test file is stack-instances.test.mjs, not instances.test.mjs. It names what it tests the way stack-release.test.mjs does.
  3. Existing tests changed only to keep the instance selection off ~/.config/mosaic-dev:
    • host.test.mjs: the two bus-service tests pass a scratch MOSAIC_CONFIG that does not exist, so the default names are selected as before.
    • scripts/test-discord.sh: the service-unit block exports the same kind of scratch MOSAIC_CONFIG and puts the caller's value back after it.
    • pinned-copy.test.mjs: "copy writes nothing when an archived path is missing" removed scripts/mosaic-config.mjs. The entry point now runs that file to select the instance before it copies, so the test removes packages/webui/README.md instead, a path only the copy reads.
    • service.test.mjs and copy-service.test.mjs: the expected render deletes the @CONFIG_ENV@ line.
  4. T-I4: the design says a named instance with no --port, and one on 7332, refuse port-claimed. They exit 4, with needs --port and port-reserved (7330, 7332 and 7333). Both are usage errors that need no unit on disk, so I read them as exit 4 like the name errors. A port another installed Console holds is port-claimed: exit 1 from webui-service.sh, exit 3 from stack-release.sh.
  5. stack-release.sh --plan now runs ports_unclaimed, so the plan lists a port-claimed refusal with the others. With no claimed port the plan text is unchanged (T-I8).
  6. T-I5 checks the writer-lock pid against the broker process under the host, not the host itself. The host spawns the broker, and the broker holds bus/writer.lock.
  7. The systemctl stand-in now reads drop-ins in systemd's full order: service.d, then the unit name cut after each hyphen (mosaic-.service.d, mosaic-bus-.service.d), then the template's and the unit's own .d. Before, it read only the last two. T-I6 needs the prefix directories to exist and be read.
  8. The webui template still says "The unit sets no MOSAIC_*" (mosaic-webui.service.in:15). A named unit now sets MOSAIC_CONFIG. I left the comment as it is so the default render stays byte-identical; correcting it changes every default unit's bytes. Follow-up for Sage to rule on. Round 2: Sage ruled (sage-1474), see round 2's N3.
  9. M11 had no killer in the first mutant list. I added the T-I1 subtest "a dangling symlink at the config path is never written through, named or default": with w instead of wx, bootstrap creates the link's target.

R3: bare default literals left in code

grep -nE 'mosaic-dev|mosaic-bus@|mosaic-webui\.service|mosaic-discord@|\$BIN/mosaic\b' over the eight changed scripts, comment lines dropped. Every hit, with the reason it stays:

Line Literal Why
scripts/webui-service.sh:77 mosaic-webui.service.in the template file's path, which every instance renders from
scripts/discord-service.sh:24 [email protected] same, for Discord
scripts/discord-service.sh:162 .../mosaic-dev/config.json status reads the default's config only when no suffix is set; the next line takes MOSAIC_INSTANCE_CONFIG for a named instance. The base's line, kept so the default's status is unchanged
scripts/instance.sh:87 the default bus drop-in globs the default instance's own scope, set in the one place every script reads
scripts/instance.sh:88 the default SESSION_ENV the default's EnvironmentFile path, the base template's literal (T-I8 pins it)
scripts/instance.sh:119 the port holder's Console scan it scans every Console unit, default and named, to find a port another instance holds
scripts/stack-copy.sh:72 file="$BIN/mosaic" the default wrapper's path only; a named instance returns earlier through wrapper_instance
scripts/stack-copy.sh:106 mosaic-bus@*|mosaic-bus-*@* the unit_root case matches both the default's and a named instance's bus units
scripts/stack-release.sh:1446 LIVE_CFG default the rehearsal reads MOSAIC_CONFIG first, and instance_select exports it for a named instance (instance.sh:99), so the default path is reached only by the default instance (F9)
scripts/mosaic-config.mjs:307 DEFAULT_CONFIG root ~/.mosaic-dev the default instance's config; a named bootstrap writes ~/.mosaic-NAME

The unit names themselves come from scripts/instance.sh:80-85, built from $MOSAIC_UNIT_SUFFIX, so they don't match the grep.

Run environment

  • Every run is in the git worktree /mnt/storage/scratch/darkwing-r190 (da230ca48 plus the candidate).
  • TMPDIR is /mnt/storage/scratch/tmp/dw190, 30 bytes, within the cli suite's limit (row 177).
  • T-I5's longest socket path is TMPDIR + /si-XXXXXX/h/.mosaic-alpha/bus/broker.sock, 72 bytes, under the kernel's 107.
  • No test reads or writes ~/.mosaic-dev, ~/.config/mosaic-dev, a live unit or a slot file. HOME is a scratch directory in every new test.

Results

Builder gate

~/darkwing-scratch/r190/gate.sh, 2026-10-20T20:07:11Z-20:17:52Z, after busy.sh was clear on two checks 20 s apart. 17 steps, each passing, counts summed by awk over the gate log:

Step Pass Fail
packages/cli node --test tests/*.test.mjs 501 0
packages/conversation 182 0
packages/control-board 124 0
packages/discord 183 0
packages/plugins harness.test.mjs 9 0
packages/webui service.test.mjs 31 0
packages/webui pinned-copy.test.mjs 10 0
scripts/test-auth.sh 15 0
scripts/test-conductor.sh 17 0
scripts/test-config.sh 24 0
scripts/test-discord.sh 78 0
scripts/test-extension-package.sh 18 0
scripts/test-foundation.sh 44 0
scripts/test-land-work.sh 86 0
scripts/test-queue.sh 156 node tests, 28 suite checks 0
scripts/test-release.sh 14 0
scripts/test-task.sh 98 0

The node tests total 1196 and the selftests 422. test-task's compose ran as project darkwing-r190, and the EXIT trap removed its network with no containers on it.

An earlier gate run (20:01:24Z) overlapped Launchpad's row 200 mutants, because busy.sh didn't match their directory. I killed it and threw away its results. busy.sh now matches every scratch/<seat>-r<N>/ directory.

Mutants

~/darkwing-scratch/r190/mutants.sh, 2026-10-20T20:17:53Z-20:27:16Z, after the gate in the same slot. First three controls with no change, each passing: SI (stack-instances.test.mjs), T55 (--test-name-pattern='^T55 ' on stack-release.test.mjs) and FIX (webui service.test.mjs, then discord copy-service.test.mjs).

Each mutant does the following, and the script stops at the first step that fails:

  • back up the file and check the backup's sha256;
  • make one exact replacement (count 1);
  • run a syntax check;
  • run its killers;
  • put the file back and check its sha256.

Every restore matched.

# File Replacement Killers run Fail Failing tests
M1 instance.sh local s="" T55 7 T-I3, T-I4, both T-I6, T-I7, T-I9
M2 instance.sh the port holder's match line becomes : T55 2 T-I4
M3 mosaic-config.mjs return; at the top of checkClaim SI, T55 7 + 2 five T-I1 and T-I7 subtests in SI; T-I7 in T55
M4 bus-service.sh adds -e '/Environment=MOSAIC_CONFIG=/d' to the render T55 5 T-I3, both T-I6, T-I9
M5 instance.sh a named SESSION_ENV set to the default's path T55 2 T-I3
M6 instance.sh a named BUS_GLOB='mosaic-bus*@*.service' T55 6 T-I9, and T-I3, T-I4 and both T-I6 through their seeds
M7 instance.sh /^@CONFIG_ENV@$/d becomes s/^@CONFIG_ENV@$// FIX 6 six webui service.test.mjs tests that compare the default render byte for byte
M8 stack-release.sh capture copies $BIN/mosaic T55 3 both T-I6
M9 mosaic-config.mjs if (false) { for the mismatch SI 3 T-I2 row 4 and the entry-point subtest
M10 mosaic-config.mjs the name pattern allows - SI 4 T-I1 bad names, T-I2 entry point
M11 mosaic-config.mjs "w" instead of "wx" SI 2 T-I1 dangling symlink
M12 guard.mjs for (const name of []) SI 1 T-I10
M13 mosaic-config.mjs the no-flag branch's checkClaim dropped SI, T55 3 + 2 T-I7's two D1 row 2 subtests and the default-on-alpha's-marker one; T-I7 in T55
M14 stack-release.sh the capture loop adds mosaic-bus-.service.d T55 3 both T-I6
M15 guard.mjs the unconditional ~/.mosaic-NAME push dropped SI 1 T-I10 (.mosaic-alpha/x under a corrupt config)

The fail counts are the runner's ℹ fail lines and include each failing test's parent. Under M4, each T-I6 subtest runs about 122 s before it fails, because without MOSAIC_CONFIG the stand-in bus waits for a host that never comes up. FIX stops at the webui file under M7, so the discord file didn't run there.

Candidate

  • build.patch: sha256 6389af4fe8fb7a49ac561adb586474e8328cc401d95ae985c385ca73484bd5e4, 2611 lines. It is git diff da230ca48 over the 24 paths, with the two new files added as intent-to-add for the diff only.
  • candidate-manifest.sha256: sha256 068828bcb5c75033aa441037bf3e9b84de0ac0f8a624aa0d0bc5a32fb9198901, 24 lines.
  • The patch applies on da230ca48 in a fresh detached worktree, and sha256sum -c of the manifest passes there. I built both after the mutant run had ended and its restores had matched. A first build during the run held M4's bus-service.sh; I deleted it unread.
  • The review candidate is commit 8862feccc9c6c96088ce934d984e00731da0b425 on local branch darkwing/row190, parent da230ca48. It is not pushed. The queue refused the manifest as a candidate, because PATH_RE (packages/queue/src/queue.mjs:37) has no @ and packages/discord/systemd/[email protected] is one of the 24 paths. Each path's blob in the commit hashes to its manifest line, with zero diff lines.

For Sage's gate

  • Run the cli suite and every scripts/test-*.sh from a git worktree of 8862feccc, with TMPDIR at 30 bytes or less.
  • No test pins a changed file by sha256 (grep of packages/*/tests and scripts/test-*.sh for each changed path and a 64-hex string found none).

Follow-ups

  • The Console and Discord uninstall and status scans read only their own instance's drop-ins; a scan of the shared prefix directories is out of this row.
  • The webui template comment (deviation 8).

Round 2

Filbert's round 1 (comment 29634): B1 blocking, N1-N7 not blocking. Sage's sage-1474 ruled N3 and left the rest to me.

B1: rebase

Row 189 (ace27dc5e) landed after da230ca48 on scripts/stack-release.sh, stack-release.test.mjs and docs/TOOLS.md. I rebased onto refactor 088087ac9 on a new local branch, darkwing/row190-r2, because #426(a) refuses a force-push to darkwing/row190. The rebased commit is ceaf93837.

  • scripts/stack-release.sh: one conflict in plan(). Row 189 added echo "$SEND_PLAN" before the Console line, and row 190 added $flags to the Console and CLI lines. The resolution keeps both. $flags is set four lines above, from INSTANCE_FLAGS.
  • stack-release.test.mjs merged with no conflict, but both rows had named a test T55. Row 190's is now T56 (T56 row 190: named instances side by side ...). The comment in stack-instances.test.mjs and the mutant script's killer pattern now say T56. Round 1's tables above keep the name T55, the name it had when they ran.
  • docs/TOOLS.md merged with no conflict.

After the rebase, T55 (row 189, 7 subtests) and T56 (row 190, 6 subtests) passed together, 15 tests and 0 failures, before any round 2 edit.

Notes

  • N1, TOOLS.md. The bus-service block and the discord-service line now show [--instance NAME]. webui-service.sh --instance NAME copy used to drop the flag without a word, while stack-copy.sh copy --instance exits 4. Now both refuse it with exit 4, because every instance shares the copies. The webui usage puts [--instance NAME] on the verbs that take it, and TOOLS.md says both copy verbs refuse it. New T-I2 subtest in stack-instances.test.mjs: "copy takes no --instance in either script". Mutant M17 below.
  • N2, T-I8. Deviation 10 below.
  • N3, Sage's ruling (sage-1474): the named-only sed rewrites the Console comment "The unit sets no MOSAIC_, CLAUDE, CODEX* or PI_* variable." to "The unit sets MOSAIC_CONFIG and no other MOSAIC_, CLAUDE, CODEX* or PI_* variable." The default keeps the line byte for byte. T56's T-I3 pins both. It checks the named Console render for the new line and for no "sets no MOSAIC_", and the default render for the template's line and for no MOSAIC_CONFIG. Mutants M16 and M19.
  • N4, instance_port_holder reads only the unit file's ExecStart=, so a drop-in that overrides ExecStart is missed. Moot on this host (no Console drop-in sets one), so it's a follow-up.
  • N5, TOOLS.md said the config directory is 0700. Bootstrap creates either directory 0700, and one that already exists keeps its mode. TOOLS.md now says that. The code is unchanged: changing the mode of a directory the user already made isn't bootstrap's job.
  • N6, stack-copy.sh check --instance NAME now also needs the wrapper's line 4 to be export MOSAIC_CONFIG='<NAME's config>', as wrapper_instance writes it. If it isn't, the CLI line reads <copy> (MOSAIC_CONFIG mismatch), the same form as unit_root's (ExecStart mismatch), and check exits
    1. The new test in packages/cli/tests/stack-copy.test.mjs, "row 190 N6", covers a good wrapper, one with another instance's config and one with no config line. With the check line disabled, the test failed (exit 1); restored, it passed. Mutant M18.
  • N7, discord-service.sh status for the default reads ~/.config/mosaic-dev/config.json and not MOSAIC_CONFIG. That's base behavior (R3 table, discord-service.sh:162), and D1 row 2 says MOSAIC_CONFIG alone selects that config. Follow-up for Sage, not changed here, so the default's status stays as it is.

Deviations, round 2

  1. T-I8 is a one-shot, not a committed test (Filbert N2). It ran once against da230ca48 (ti8/), and no committed test compares the full default bus render with the base's. Committed tests pin the default Console and Discord renders byte for byte against their templates (service.test.mjs, copy-service.test.mjs), plus the default bus render's lines that host.test.mjs checks. I didn't rerun T-I8 on 088087ac9. Row 189 changed none of the 11 files it compares except stack-release.sh, and that change is the send: line, which shows on both sides.
  2. packages/cli/tests/stack-copy.test.mjs joins the manifest for N6, so there are 25 paths.
  3. Round 2 adds four mutants, M16-M19, on top of the design's M1-M15. Each one guards one of the round 2 changes.

Gate and mutants, round 2

Sage's slot sage-grant-190-r2 (sage-1478). ~/darkwing-scratch/r190/run-all.sh waited for busy.sh to be clear on two checks 20 s apart, then ran the gate and the mutants on 537a02392 in the r190 worktree.

Gate, 2026-10-20T20:56:42Z-21:07:41Z. 17 steps, each rc=0. Counts are awk sums over the gate logs:

Step Pass Fail
packages/cli node --test tests/*.test.mjs 511 0
packages/conversation 182 0
packages/control-board 124 0
packages/discord 183 0
packages/plugins harness.test.mjs 9 0
packages/webui service.test.mjs 31 0
packages/webui pinned-copy.test.mjs 10 0
scripts/test-auth.sh 15 0
scripts/test-conductor.sh 17 0
scripts/test-config.sh 24 0
scripts/test-discord.sh 78 0
scripts/test-extension-package.sh 18 0
scripts/test-foundation.sh 44 0
scripts/test-land-work.sh 86 0
scripts/test-queue.sh 156 node tests, 28 suite checks 0
scripts/test-release.sh 14 0
scripts/test-task.sh 98 0

The node tests total 1206 and the selftests 422. pkg-cli has 10 more than round 1: row 189's T55 (7) and round 2's N1 and N6 subtests. test-task's compose ran as project darkwing-r190, and the EXIT trap removed its network.

Mutants, 2026-10-20T21:07:42Z-21:18:02Z, with the same steps as round 1. Four controls with no change, each exit 0: SI, T56 (--test-name-pattern='^T56 ' on stack-release.test.mjs), SC (--test-name-pattern='^row 190 N6' on stack-copy.test.mjs) and FIX. Every restore matched. 19 run, 19 killed, 0 survived:

# Killers run Fail Failing tests
M1 T56 7 T-I3, T-I4, both T-I6, T-I7, T-I9
M2 T56 2 T-I4
M3 SI, T56 9 T-I1 and T-I7 subtests in SI, T-I7 in T56
M4 T56 5 T-I3, both T-I6, T-I9
M5 T56 2 T-I3
M6 T56 6 T-I3, T-I4, both T-I6, T-I9
M7 FIX 6 the webui service.test.mjs tests that compare the default render byte for byte
M8 T56 3 both T-I6
M9 SI 3 T-I2 row 4 and the entry-point subtest
M10 SI 4 T-I1 bad names, T-I2 entry point
M11 SI 2 T-I1 dangling symlink
M12 SI 1 T-I10
M13 SI, T56 5 T-I7's D1 row 2 subtests, T-I7 in T56
M14 T56 3 both T-I6
M15 SI 1 T-I10
M16 T56 2 T-I3 (the named Console comment kept)
M17 SI 2 T-I2 "copy takes no --instance in either script"
M18 SC 1 row 190 N6
M19 T56 2 T-I3 (the default Console comment rewritten)

M16 drops the named sed line for the comment, M17 makes webui-service.sh copy accept --instance, M18 skips the wrapper config check in stack-copy.sh check, and M19 adds the comment rewrite to the default's sed. Fail counts are the runner's ℹ fail lines, parents included. The M3 count is 9 here against 7 + 2 in round 1 because this table sums both killers.

Candidate, round 2

  • The review candidate is commit 537a02392bf5eeecf3f55dbfec5162565b786456 on darkwing/row190-r2 (pushed), parent ceaf93837, which is round 1's change rebased onto refactor 088087ac9. It's a commit, not the manifest, because PATH_RE still refuses the @ path.
  • build-r2.patch: sha256 0ba1aa3ec6b466055754893c33c8934eb1323627cb4ef32b163d18b489421f8b, 2707 lines, git diff 088087ac9 537a02392 over the 25 paths.
  • candidate-manifest-r2.sha256: sha256 2296e9cb130eafdd79515aec364393e1493dad81e10a2e57e4ac4ba96f024e0f, 25 lines.
  • The patch applies with git apply --index on 088087ac9 in a fresh detached worktree, sha256sum -c of the manifest passes there, and each path's blob there equals its blob in 537a02392. I built both after the mutant run ended and its restores matched.
  • Round 1's build.patch and candidate-manifest.sha256 stay as round 1's record.
  • No test pins a changed file by sha256. The only test with a 64-hex string that names a changed file is packages/webui/tests/service.test.mjs, and its string is a fake UID (:21).

For Sage's gate: run the cli suite and every scripts/test-*.sh from a git worktree of 537a02392, with TMPDIR at 30 bytes or less.

Follow-ups, round 2

  • N4: instance_port_holder and a drop-in ExecStart override.
  • N7: the default discord-service.sh status and MOSAIC_CONFIG (D1 row 2).
  • PATH_RE and @ paths (reported to Sage in round 1).
  • Round 1's first follow-up (drop-in scans) stands. The webui template comment is settled by N3.

Round 3

Filbert's round 2 (comment 29685) had one blocking item, B1. Row 192 (4429a104f, Q175 phase 1 SSO) landed at 2026-10-20T21:21:29Z, after round 2's candidate and gate. Row 192 touches four of the 25 paths: scripts/mosaic-config.mjs, scripts/webui-service.sh, packages/webui/tests/service.test.mjs and packages/webui/README.md. Round 2's own changes needed nothing more.

B1: rebase

I rebased onto refactor 176a6bf4b on a new local branch, darkwing/row190-r3, because #426(a) refuses a force-push. The commits:

  • ab1d2ee9e: round 1's change, rebased.
  • cdac7ba92: round 2's change, rebased.
  • df65a3d4c: round 3's only new code, the T-I3 assertion below.

I didn't run npm ci in the worktree. Its node_modules is a symlink to the canonical checkout's, which already has row 192's oauth4webapi 3.8.8. The webui suite's SSO tests passed on it in the gate.

Filbert's merge hazards, each checked on df65a3d4c:

  • Row 192 makes validate and load async. Every call to load in mosaic-config.mjs is awaited: in bootstrapDefault (:350, :356, :359), bootstrapNamed (:368, :394, :398), selected (:415, :420) and loadSelected (:442). Those four functions are async now, and the command switch awaits each one (:457-483). checkClaim stays sync, and every caller passes it a dataRoot from an awaited load, so it never gets a Promise.
  • The top-level try { switch ... } catch (:487-490) awaits everything inside it, so a Failure thrown from row 192's validateConsole still exits with its own code. scripts/test-config.sh proves it for the default: five bad console bodies through validate each exit 2 and name the key path. That suite has 31 checks now, up from 24 in round 2, and the 7 new ones are row 192's. No test sends a bad console through a named instance. That path is the same loadSelected to load to validate chain with instance set.
  • webui-service.sh keeps row 192's --sso comment (:52), flag (:143), refusals (:221-227) and unit flag (:260), plus row 190's instance block (:86-91). USAGE has one FLAGS line: [--sso [--send] | --public-host NAME --auth-header HEADER --allow-user ID [--send]]. In service.test.mjs, row 192's tests merged with round 1's @CONFIG_ENV@ line removal in the expected render, with no conflict.
  • Filbert note 4. A named unit's Environment=MOSAIC_CONFIG= gives each instance its own console.sso (read through loadSystem) and its own slots (business.mjs configDir()). T56's T-I3 now renders alpha's Console with --sso and checks that the unit carries both --sso on ExecStart and alpha's MOSAIC_CONFIG line. That's 6 lines in stack-release.test.mjs. Mutant M20 below drops --sso from the named render.

Deviations, round 3

  1. Filbert asked for the 19 mutants on the new base. I ran 20: M1-M19 unchanged, plus M20 for note 4's assertion.

Gate and mutants, round 3

Sage's slot (sage-1512). ~/darkwing-scratch/r190/run-all.sh waited for busy.sh to be clear on two checks 20 s apart, then ran the gate and the mutants on df65a3d4c in the r190 worktree.

Gate, 2026-10-20T21:47:27Z-21:59:13Z. 17 steps, each rc=0. Counts are awk sums over gate-r3.txt:

Step Pass Fail
packages/cli node --test tests/*.test.mjs 511 0
packages/conversation 182 0
packages/control-board 124 0
packages/discord 183 0
packages/business 149 0
packages/webui (whole suite) 299, 1 skipped 0
packages/plugins harness.test.mjs 9 0
scripts/test-auth.sh 15 0
scripts/test-conductor.sh 17 0
scripts/test-config.sh 31 0
scripts/test-discord.sh 78 0
scripts/test-extension-package.sh 18 0
scripts/test-foundation.sh 44 0
scripts/test-land-work.sh 86 0
scripts/test-queue.sh 156 node tests, 28 suite checks 0
scripts/test-release.sh 14 0
scripts/test-task.sh 98 0

That's 2042 passed: 1613 node tests and 429 selftests. Round 2 had 1206 and 422. This gate adds packages/business (149) and runs the whole webui suite (299) instead of its two service files (41). Those two changes account for the 407 extra node tests. The 7 extra selftests are row 192's test-config.sh checks. pkg-cli is still 511, and that includes T56's new T-I3 assertion, which sits inside an existing subtest. test-task's compose ran as project darkwing-r190, and the EXIT trap removed its network.

Row 192's re-pin activate (W5) ran at 22:02:09Z, after the gate and during the mutants. Nothing in the gate or the mutants uses the live units. M4's two T-I6 cases ran across 22:02. Each one timed out at about 122 s, against 4 s in the control. Rounds 1 and 2 show the same 122 s timeouts under M4 (mut-r1/, mut-r2/), so the mutant causes them, not the activate.

Mutants, 2026-10-20T21:59:14Z-22:09:37Z, with the same steps as round 2. The four controls (SI, T56, SC, FIX) each exited 0. Every restore matched, and the worktree ended at df65a3d4c with no changes. 20 run, 20 killed, 0 survived:

# Killers run Fail Failing tests
M1 T56 7 T-I3, T-I4, both T-I6, T-I7, T-I9
M2 T56 2 T-I4
M3 SI, T56 9 T-I1 and T-I7 subtests in SI, T-I7 in T56
M4 T56 5 T-I3, both T-I6, T-I9
M5 T56 2 T-I3
M6 T56 6 T-I3, T-I4, both T-I6, T-I9
M7 FIX 6 the webui service.test.mjs tests that compare the default render byte for byte
M8 T56 3 both T-I6
M9 SI 3 T-I2 row 4 and the entry-point subtest
M10 SI 4 T-I1 bad names, T-I2 entry point
M11 SI 2 T-I1 dangling symlink
M12 SI 1 T-I10
M13 SI, T56 5 T-I7's D1 row 2 subtests, T-I7 in T56
M14 T56 3 both T-I6
M15 SI 1 T-I10
M16 T56 2 T-I3 (the named Console comment kept)
M17 SI 2 T-I2 "copy takes no --instance in either script"
M18 SC 1 row 190 N6
M19 T56 2 T-I3 (the default Console comment rewritten)
M20 T56 2 T-I3 (the named --sso render)

M1-M19 fail the same tests, with the same counts, as round 2. M20 replaces [ "$SSO" = 0 ] || flags="$flags --sso" in webui-service.sh with :, so a named --sso render loses --sso. Fail counts are the runner's ℹ fail lines, parents included.

Candidate, round 3

  • The review candidate is commit df65a3d4c8fec64ba3aba20a481c244f9d73a84d on darkwing/row190-r3 (pushed), base refactor 176a6bf4b. It's a commit, not the manifest, because PATH_RE still refuses the @ path.
  • build-r3.patch: sha256 efe898c2ce2de5c98cf0354746aa3b7ba8c61467499ccf67d62a9fa33a59ad04, 2713 lines, git diff 176a6bf4b df65a3d4c over the 25 paths, the same path set as round 2.
  • candidate-manifest-r3.sha256: sha256 cc0ea9e8f352a4805f8cda6dae8c2401880bed32fc10f25fee4ae7091c7970af, 25 lines.
  • I built both from git objects, not from the worktree the mutants edit. The patch applies with git apply --index on 176a6bf4b in a fresh detached worktree, sha256sum -c of the manifest passes there, and that tree equals df65a3d4c's.
  • No test pins a changed file by sha256. Two test files with a 64-hex string name a changed path. In packages/webui/tests/service.test.mjs the string is the fake UID (:21), and in packages/queue/tests/fixtures/queue-marked.md it's an old manifest digest in history text (:119).

For Sage's gate: run the cli suite and every scripts/test-*.sh from a git worktree of df65a3d4c, with node_modules linked and TMPDIR at 30 bytes or less.

Follow-ups, round 3

Unchanged from round 2: N4 (instance_port_holder and a drop-in ExecStart), N7 (the default discord-service.sh status and MOSAIC_CONFIG), and PATH_RE with @ paths. Row 230 (C2-13a) leaves out row 190's @CONFIG_ENV@ and @SESSION_ENV@ tokens (gosalyn-0058 item 4). Whichever of the two rows lands second adds them.

Filbert approved round 3 (Gitea 29713, queue rev 2504, 2026-10-20T22:15:59Z). One more follow-up came from that review, not blocking: no test sends a bad console body through a named instance's validate.