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.
33 KiB
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'sEnvironmentFilebecomes@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 inpackages/cli/tests/host.test.mjs,packages/webui/tests/service.test.mjs,packages/webui/tests/pinned-copy.test.mjs,packages/discord/tests/copy-service.test.mjsandscripts/test-discord.sh. - Docs:
AGENTS.md(the two lines in Mos's wording, sage-1335),docs/TOOLS.md(the--instancesection, 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 renderwith and without--pm,webui-service.sh renderwith and without--tester-port,discord-service.sh render, andstack-copy.sh checkwith and without--business;stack-release.sh status, fouractivate ... --planforms (C1, C1 with--tester-port, C with--migrate, and B, which refuses) androllback --plan(which refuses);- after a real
activate C1,statusandrollback --planagain.
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
- Names alpha and beta, not the design's
aandb. The D1 rule^[a-z][a-z0-9]{1,15}$needs two characters or more, soais invalid. - The new test file is
stack-instances.test.mjs, notinstances.test.mjs. It names what it tests the waystack-release.test.mjsdoes. - Existing tests changed only to keep the instance selection off
~/.config/mosaic-dev:host.test.mjs: the two bus-service tests pass a scratchMOSAIC_CONFIGthat does not exist, so the default names are selected as before.scripts/test-discord.sh: the service-unit block exports the same kind of scratchMOSAIC_CONFIGand puts the caller's value back after it.pinned-copy.test.mjs: "copy writes nothing when an archived path is missing" removedscripts/mosaic-config.mjs. The entry point now runs that file to select the instance before it copies, so the test removespackages/webui/README.mdinstead, a path only the copy reads.service.test.mjsandcopy-service.test.mjs: the expected render deletes the@CONFIG_ENV@line.
- T-I4: the design says a named instance with no
--port, and one on 7332, refuseport-claimed. They exit 4, withneeds --portandport-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 isport-claimed: exit 1 fromwebui-service.sh, exit 3 fromstack-release.sh. stack-release.sh --plannow runsports_unclaimed, so the plan lists aport-claimedrefusal with the others. With no claimed port the plan text is unchanged (T-I8).- 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. - The
systemctlstand-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. - The webui template still says "The unit sets no MOSAIC_*" (
mosaic-webui.service.in:15). A named unit now setsMOSAIC_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. - 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
winstead ofwx, 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(da230ca48plus 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.HOMEis 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: sha2566389af4fe8fb7a49ac561adb586474e8328cc401d95ae985c385ca73484bd5e4, 2611 lines. It isgit diff da230ca48over the 24 paths, with the two new files added as intent-to-add for the diff only.candidate-manifest.sha256: sha256068828bcb5c75033aa441037bf3e9b84de0ac0f8a624aa0d0bc5a32fb9198901, 24 lines.- The patch applies on
da230ca48in a fresh detached worktree, andsha256sum -cof 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'sbus-service.sh; I deleted it unread. - The review candidate is commit
8862feccc9c6c96088ce934d984e00731da0b425on local branchdarkwing/row190, parentda230ca48. It is not pushed. The queue refused the manifest as a candidate, becausePATH_RE(packages/queue/src/queue.mjs:37) has no@andpackages/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-*.shfrom a git worktree of 8862feccc, with TMPDIR at 30 bytes or less. - No test pins a changed file by sha256 (grep of
packages/*/testsandscripts/test-*.shfor 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 inplan(). Row 189 addedecho "$SEND_PLAN"before the Console line, and row 190 added$flagsto the Console and CLI lines. The resolution keeps both.$flagsis set four lines above, fromINSTANCE_FLAGS.stack-release.test.mjsmerged 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 instack-instances.test.mjsand 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.mdmerged 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 copyused to drop the flag without a word, whilestack-copy.sh copy --instanceexits 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 instack-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_holderreads only the unit file'sExecStart=, so a drop-in that overridesExecStartis 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 NAMEnow also needs the wrapper's line 4 to beexport MOSAIC_CONFIG='<NAME's config>', aswrapper_instancewrites it. If it isn't, the CLI line reads<copy> (MOSAIC_CONFIG mismatch), the same form asunit_root's(ExecStart mismatch), and check exits- 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.
- The new test in
- N7,
discord-service.sh statusfor the default reads~/.config/mosaic-dev/config.jsonand notMOSAIC_CONFIG. That's base behavior (R3 table,discord-service.sh:162), and D1 row 2 saysMOSAIC_CONFIGalone selects that config. Follow-up for Sage, not changed here, so the default's status stays as it is.
Deviations, round 2
- 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 thathost.test.mjschecks. I didn't rerun T-I8 on088087ac9. Row 189 changed none of the 11 files it compares exceptstack-release.sh, and that change is thesend:line, which shows on both sides. packages/cli/tests/stack-copy.test.mjsjoins the manifest for N6, so there are 25 paths.- 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
537a02392bf5eeecf3f55dbfec5162565b786456ondarkwing/row190-r2(pushed), parentceaf93837, which is round 1's change rebased onto refactor088087ac9. It's a commit, not the manifest, becausePATH_REstill refuses the@path. build-r2.patch: sha2560ba1aa3ec6b466055754893c33c8934eb1323627cb4ef32b163d18b489421f8b, 2707 lines,git diff 088087ac9 537a02392over the 25 paths.candidate-manifest-r2.sha256: sha2562296e9cb130eafdd79515aec364393e1493dad81e10a2e57e4ac4ba96f024e0f, 25 lines.- The patch applies with
git apply --indexon088087ac9in a fresh detached worktree,sha256sum -cof the manifest passes there, and each path's blob there equals its blob in537a02392. I built both after the mutant run ended and its restores matched. - Round 1's
build.patchandcandidate-manifest.sha256stay 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_holderand a drop-inExecStartoverride. - N7: the default
discord-service.sh statusandMOSAIC_CONFIG(D1 row 2). PATH_REand@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
validateandloadasync. Every call toloadinmosaic-config.mjsis awaited: inbootstrapDefault(:350, :356, :359),bootstrapNamed(:368, :394, :398),selected(:415, :420) andloadSelected(:442). Those four functions are async now, and the command switch awaits each one (:457-483).checkClaimstays sync, and every caller passes it adataRootfrom an awaitedload, 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'svalidateConsolestill exits with its own code.scripts/test-config.shproves it for the default: five badconsolebodies throughvalidateeach 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 badconsolethrough a named instance. That path is the sameloadSelectedtoloadtovalidatechain withinstanceset. webui-service.shkeeps row 192's--ssocomment (: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]]. Inservice.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 ownconsole.sso(read throughloadSystem) and its own slots (business.mjsconfigDir()). T56's T-I3 now renders alpha's Console with--ssoand checks that the unit carries both--ssoonExecStartand alpha'sMOSAIC_CONFIGline. That's 6 lines instack-release.test.mjs. Mutant M20 below drops--ssofrom the named render.
Deviations, round 3
- 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
df65a3d4c8fec64ba3aba20a481c244f9d73a84dondarkwing/row190-r3(pushed), base refactor176a6bf4b. It's a commit, not the manifest, becausePATH_REstill refuses the@path. build-r3.patch: sha256efe898c2ce2de5c98cf0354746aa3b7ba8c61467499ccf67d62a9fa33a59ad04, 2713 lines,git diff 176a6bf4b df65a3d4cover the 25 paths, the same path set as round 2.candidate-manifest-r3.sha256: sha256cc0ea9e8f352a4805f8cda6dae8c2401880bed32fc10f25fee4ae7091c7970af, 25 lines.- I built both from git objects, not from the worktree the mutants edit. The patch applies with
git apply --indexon176a6bf4bin a fresh detached worktree,sha256sum -cof 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.mjsthe string is the fake UID (:21), and inpackages/queue/tests/fixtures/queue-marked.mdit'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.