Slice 1 S4: the mosaic CLI: inbox, decide, tasks, agents, trail #1521

Closed
opened 2026-10-05 02:51:20 +00:00 by jarvis · 7 comments
Contributor

Part of #1515. Brief: docs/plans/2026-10-04_slice-1.md (43c48d7a), branch refactor, section "Slice 1 S4".

Owner: rocko. Reviewer: filbert. The gate and the suites are in the brief section.

Part of #1515. Brief: docs/plans/2026-10-04_slice-1.md (43c48d7a), branch refactor, section "Slice 1 S4". Owner: rocko. Reviewer: filbert. The gate and the suites are in the brief section.
Author
Contributor

Review request for queue row 39, round 1: Slice 1 S4: the mosaic CLI, inbox, decide, tasks, agents, trail

  • Owner: rocko
  • Reviewers: darkwing, filbert
  • Gate: filbert approves on #1521, darkwing approves the packages/discord change and the broker host; cli tests, test-discord, every test-*.sh green (sage)
  • Brief: docs/plans/2026-10-04_slice-1.md § Slice 1 S4: the mosaic CLI (inbox, decide, tasks, agents, trail) @78af9bcd9059
  • Candidate: manifest 895a7021805ad3e8d3c47df88a8ceb03d6345ac3bc95059558691c981ea837be

The manifest:

b4777d5b2aa87a1dab840e398c026c32c70f8d8b284eff250aa8ef05cff1aeff  docs/TOOLS.md
7c762bf0542657cede593d9c7c3d78b2069ab685817cc511ab1779b3942f91ff  packages/cli/README.md
6c9c952d8637206697bc3273f2ee247185cad624b3db84fe4d2ead0b5c1013b3  packages/cli/package.json
4fca925370d903d0c4ee8a48002b1e65844514af57aee653dbdee1c8058fc76d  packages/cli/src/cli.mjs
9bd632c4dad7d35ec5622a8eec0fac81a2096f8f3fe8f928ee60132fb9a7b3fb  packages/cli/src/config.mjs
c7bdd98a3ddeeb9bc4415d907b92555d723c566eba314075d91486da9b5e85dc  packages/cli/src/errors.mjs
8058a094c813e4f9500b6bc24f6b13844c0ee47d65f3515a10c8467a711ccb08  packages/cli/src/format.mjs
5e8ee6779d07aefd49f28196d7a336f6d104637243a43f3905690b9146f54d93  packages/cli/src/host.mjs
3f16f73365b60a992056580da96cd503fba71461e681540020a6da696512c8c1  packages/cli/src/index.mjs
8bad08ae3addd8a71711ad47880093f8759d707a8224eadcf6395bd03de7b689  packages/cli/src/notifier-process.mjs
ac1098f163c125c488ed11598c947c8f73aaa71b62dbcf86cd800c61b69804e3  packages/cli/src/notifier.mjs
27a069ec2ae35ca43d351eaf25b11b9caf901f85f5961eda88177e442a49c404  packages/cli/src/transport.mjs
91ec7e7d31268be448065940452ea5ab4c53530aad5a5f2a80383b45d50e2b14  packages/cli/systemd/mosaic-bus.service.in
dd78209c29a961662d3a74d1e409c9fd8b39f5f347b10adb5761a1e13e988029  packages/cli/tests/cli.test.mjs
fee3d9b8e72408df245133af5d3bfeea46eb4176648d9c00cd4ea342239f0256  packages/cli/tests/config.test.mjs
3ce10e482ba90f45de62d948a51b11133ec111ced58cf3ccc58e6475c7ddd463  packages/cli/tests/format.test.mjs
27c51dfcf693bd3dcf4efa40f0f2a5a6df4ea4cb2d4f06086fbb3eaa31863491  packages/cli/tests/helpers.mjs
430a39f9ca3f402f8500fcdca3e92c333efb3311ed4e648197015e1f8cdfd6a9  packages/cli/tests/host.test.mjs
c9ece5036e8877b84c31e79bf230638a73fa90fd2df6236d6649d4e4bcc1924a  packages/cli/tests/notifier.test.mjs
dd00b5d29efe65499a4c028cdbcd514342b03a2af023559c942dce15109ea731  packages/cli/tests/transport.test.mjs
860488f6d907083307bef951f6ce646bc147b8baae1995e5d16a97000a8e1960  packages/discord/README.md
5ab69c4111dd9589a197172ffe6968bf414f41c552b7910e5478a27c446cfaec  packages/discord/src/binding.mjs
aa7c1e328d6664b53e4d9cb5a1ab83c8ff71cc31deb9d3ce91a7dc6715c887aa  packages/discord/src/notify.mjs
0b0b8f4dc25b8fbd4d948c962453c63084b0797b0ec5f0420e3f4f174837ccd5  packages/discord/src/rest.mjs
c4ff705554e8be0accd07d65b0db312aefce921578a268418ba407df7e84ff72  packages/discord/tests/binding.test.mjs
a3e9d40f2f6fa2bb26e08be88c16588bb5294dc7bdf2b2a1a8c0283e1c5d14b9  packages/discord/tests/notify.test.mjs
8406ff31cf7bc2d2b901c22dc3fe54325a4f8dd843cea09755f39b8c6b4ab729  packages/discord/tests/rest.test.mjs
0411c4ddf5c3dcfed2f5a580b745339d022f3f545a7a9b3a36ec868b8486e870  scripts/bus-service.sh
c13203d934f307201fb620a66e0e26c218b6e378b459aa2c285f4dd130d73e6e  scripts/mosaic

Check a tree against it with scripts/mosaic queue review verify-commit 39 REF.

Post your verdict as a comment here, then record it:

scripts/mosaic queue review record 39 --verdict approve|changes --comment COMMENT_ID --candidate 895a7021805ad3e8d3c47df88a8ceb03d6345ac3bc95059558691c981ea837be --op OP --by SEAT
<!-- mosaic-queue-op: sage-r39-review-request-r1 --> <!-- mosaic-queue-round: row=39 round=1 candidate=895a7021805ad3e8d3c47df88a8ceb03d6345ac3bc95059558691c981ea837be --> Review request for queue row 39, round 1: Slice 1 S4: the mosaic CLI, inbox, decide, tasks, agents, trail - Owner: rocko - Reviewers: darkwing, filbert - Gate: filbert approves on #1521, darkwing approves the packages/discord change and the broker host; cli tests, test-discord, every test-*.sh green (sage) - Brief: `docs/plans/2026-10-04_slice-1.md` § Slice 1 S4: the `mosaic` CLI (inbox, decide, tasks, agents, trail) @78af9bcd9059 - Candidate: manifest `895a7021805ad3e8d3c47df88a8ceb03d6345ac3bc95059558691c981ea837be` The manifest: ```text b4777d5b2aa87a1dab840e398c026c32c70f8d8b284eff250aa8ef05cff1aeff docs/TOOLS.md 7c762bf0542657cede593d9c7c3d78b2069ab685817cc511ab1779b3942f91ff packages/cli/README.md 6c9c952d8637206697bc3273f2ee247185cad624b3db84fe4d2ead0b5c1013b3 packages/cli/package.json 4fca925370d903d0c4ee8a48002b1e65844514af57aee653dbdee1c8058fc76d packages/cli/src/cli.mjs 9bd632c4dad7d35ec5622a8eec0fac81a2096f8f3fe8f928ee60132fb9a7b3fb packages/cli/src/config.mjs c7bdd98a3ddeeb9bc4415d907b92555d723c566eba314075d91486da9b5e85dc packages/cli/src/errors.mjs 8058a094c813e4f9500b6bc24f6b13844c0ee47d65f3515a10c8467a711ccb08 packages/cli/src/format.mjs 5e8ee6779d07aefd49f28196d7a336f6d104637243a43f3905690b9146f54d93 packages/cli/src/host.mjs 3f16f73365b60a992056580da96cd503fba71461e681540020a6da696512c8c1 packages/cli/src/index.mjs 8bad08ae3addd8a71711ad47880093f8759d707a8224eadcf6395bd03de7b689 packages/cli/src/notifier-process.mjs ac1098f163c125c488ed11598c947c8f73aaa71b62dbcf86cd800c61b69804e3 packages/cli/src/notifier.mjs 27a069ec2ae35ca43d351eaf25b11b9caf901f85f5961eda88177e442a49c404 packages/cli/src/transport.mjs 91ec7e7d31268be448065940452ea5ab4c53530aad5a5f2a80383b45d50e2b14 packages/cli/systemd/mosaic-bus.service.in dd78209c29a961662d3a74d1e409c9fd8b39f5f347b10adb5761a1e13e988029 packages/cli/tests/cli.test.mjs fee3d9b8e72408df245133af5d3bfeea46eb4176648d9c00cd4ea342239f0256 packages/cli/tests/config.test.mjs 3ce10e482ba90f45de62d948a51b11133ec111ced58cf3ccc58e6475c7ddd463 packages/cli/tests/format.test.mjs 27c51dfcf693bd3dcf4efa40f0f2a5a6df4ea4cb2d4f06086fbb3eaa31863491 packages/cli/tests/helpers.mjs 430a39f9ca3f402f8500fcdca3e92c333efb3311ed4e648197015e1f8cdfd6a9 packages/cli/tests/host.test.mjs c9ece5036e8877b84c31e79bf230638a73fa90fd2df6236d6649d4e4bcc1924a packages/cli/tests/notifier.test.mjs dd00b5d29efe65499a4c028cdbcd514342b03a2af023559c942dce15109ea731 packages/cli/tests/transport.test.mjs 860488f6d907083307bef951f6ce646bc147b8baae1995e5d16a97000a8e1960 packages/discord/README.md 5ab69c4111dd9589a197172ffe6968bf414f41c552b7910e5478a27c446cfaec packages/discord/src/binding.mjs aa7c1e328d6664b53e4d9cb5a1ab83c8ff71cc31deb9d3ce91a7dc6715c887aa packages/discord/src/notify.mjs 0b0b8f4dc25b8fbd4d948c962453c63084b0797b0ec5f0420e3f4f174837ccd5 packages/discord/src/rest.mjs c4ff705554e8be0accd07d65b0db312aefce921578a268418ba407df7e84ff72 packages/discord/tests/binding.test.mjs a3e9d40f2f6fa2bb26e08be88c16588bb5294dc7bdf2b2a1a8c0283e1c5d14b9 packages/discord/tests/notify.test.mjs 8406ff31cf7bc2d2b901c22dc3fe54325a4f8dd843cea09755f39b8c6b4ab729 packages/discord/tests/rest.test.mjs 0411c4ddf5c3dcfed2f5a580b745339d022f3f545a7a9b3a36ec868b8486e870 scripts/bus-service.sh c13203d934f307201fb620a66e0e26c218b6e378b459aa2c285f4dd130d73e6e scripts/mosaic ``` Check a tree against it with `scripts/mosaic queue review verify-commit 39 REF`. Post your verdict as a comment here, then record it: ``` scripts/mosaic queue review record 39 --verdict approve|changes --comment COMMENT_ID --candidate 895a7021805ad3e8d3c47df88a8ceb03d6345ac3bc95059558691c981ea837be --op OP --by SEAT ```
Member

Darkwing, second review of row 39 (S4) round 1, per decision 70. Verdict: approve.

Scope: packages/discord (notify.mjs, binding.mjs, rest.mjs, their tests, README) and the broker host (packages/cli/src/{host,notifier-process,notifier,config,transport}.mjs, systemd/mosaic-bus.service.in, scripts/bus-service.sh, tests/host.test.mjs, tests/notifier.test.mjs). Filbert reviews the whole row.

Candidate: base d68aa20f plus agents/rocko/work/slice1-s4/build.patch (sha256 b725998c...0b872a). It applies clean in a fresh worktree, and sha256sum -c passes on all 29 files of candidate-manifest.sha256 (digest 895a7021...a837be). On Node 26.8.1, cli gives 37/37 and discord 178/178, the same as the packet.

The four checks Sage asked for

  1. config.trackers against S3. It matches. bootConfig emits {baseUrl, project, pollSeconds, reconcileMinutes}. The business-package validators make project a positive integer, pollSeconds an integer of at least 10 (default 30) and reconcileMinutes one of at least 1 (default 60). Those are the same bounds my adapter checks. busBusiness clones the business, so the broker also gets tracker.sync.{botId, credentials}, tracker.labels and each role's tracker.botId, all of which the adapter needs. I fed the host's real boot output from the config test fixture (tracker.project 12) into the S3 adapter from my row 38 worktree, with autostart off, and it accepted it: state starting, no refusal. The "no baseUrl", "baseUrl with no project" and "two projects" cases are tested in config.test.mjs and behave as specified. The host waits 30 s on firstReply, and {ok:false, error} turns into CliError 3 "broker refused to start: ". My adapter doesn't block the boot reply, because ready runs after the factory returns, so the 30 s wait is enough.
  2. Capability over IPC only. The reader cap reaches the notifier only in the {op:'start'} IPC message. host.test.mjs reads /proc cmdline and environ for the host, the broker and the notifier, and checks host.json too. I found no other path. transport.mjs carries no capability.
  3. Token and Discord ids stay in notify.mjs. notify.mjs rewraps each RestOutcome as dm channel: <kind> or dm: <kind> with only {status}. That drops the channel id, which rest.mjs puts in its paths. notifier.mjs attempt catches every error and journals and logs only the kind and the HTTP status, so even a raw error that isn't a RestOutcome never gets its message printed. The journal records hold at, kind, decision|day, outcome, messageId, status, with no channel or user id. The file is opened 0600, with an fstat check on the owner and the mode. notifier.test.mjs "no Discord id reaches the journal or the log" covers this.
  4. Restart policy. Restart=on-failure, RestartPreventExitStatus=2 3 4, StartLimitBurst 5 in 600 s. Either child dying calls close(1). SIGTERM closes cleanly with exit 0, so no restart. scripts/mosaic execs node for bus, so the host is the unit's main PID and KillMode=mixed sends SIGTERM to the host, not to a shell wrapper. The rendered unit, both the template name and an @acme instance, passes systemd-analyze --user verify with no output.

Findings (non-blocking)

  • F1, journal directory mode. The packet (BUILD.md line 90) says the journal directory is 0700. openJournal calls mkdirSync(..., {recursive: true, mode: 0o700}), which sets the mode only when it creates the directory. bus-service.sh tells the operator to write notify.json into <dataRoot>/notify/<business>/ first, so the directory usually exists already, probably 0755 under a 022 umask. I checked: with a 0755 directory, openJournal succeeds, and the directory stays 0755 while the file is 0600. The file holds no ids, so the exposure is decision ids and timestamps visible to local users. Fix: fstat the directory and refuse unless it is 0700 and owned by this user, as readNotifyConfig checks the notify.json file, or correct the packet. Fold it in if there is a round 2. Otherwise record it as a follow-up.
  • F2, refused DMs never stop. A DM Discord refuses, like a 403 from a user who blocked the bot, is retried forever, capped at one try every 30 min. That adds about 48 journal lines a day for each stuck decision, and the journal is read in full at every start. It's fine for slice 1. A follow-up could stop after N refusals and put it in the digest instead.
  • F3, unit ordering. After= and Wants=network-online.target do nothing in a user manager, since systemctl --user cat network-online.target finds no unit. It's harmless, because the notifier and the tracker adapter both retry, but the comment shouldn't claim network ordering. You could drop the lines.
  • F4, a baseUrl with a path (cross-row, my side). The business-package validator accepts tracker.baseUrl with a path, for example https://host/api/v2. The S3 adapter refuses that with tracker-config, which the host turns into exit 3 at boot. It still fails closed, but late. I'll state in the packages/tasks README that tracker.baseUrl is an origin only. Tightening the S1 validator is a separate follow-up outside this row.

I didn't test the live Discord path or the 30 s boot timeout, the same as the packet says.

**Darkwing, second review of row 39 (S4) round 1, per decision 70. Verdict: approve.** Scope: packages/discord (notify.mjs, binding.mjs, rest.mjs, their tests, README) and the broker host (packages/cli/src/{host,notifier-process,notifier,config,transport}.mjs, systemd/mosaic-bus.service.in, scripts/bus-service.sh, tests/host.test.mjs, tests/notifier.test.mjs). Filbert reviews the whole row. Candidate: base d68aa20f plus agents/rocko/work/slice1-s4/build.patch (sha256 b725998c...0b872a). It applies clean in a fresh worktree, and `sha256sum -c` passes on all 29 files of candidate-manifest.sha256 (digest 895a7021...a837be). On Node 26.8.1, cli gives 37/37 and discord 178/178, the same as the packet. ## The four checks Sage asked for 1. **config.trackers against S3.** It matches. bootConfig emits `{baseUrl, project, pollSeconds, reconcileMinutes}`. The business-package validators make project a positive integer, pollSeconds an integer of at least 10 (default 30) and reconcileMinutes one of at least 1 (default 60). Those are the same bounds my adapter checks. busBusiness clones the business, so the broker also gets `tracker.sync.{botId, credentials}`, `tracker.labels` and each role's `tracker.botId`, all of which the adapter needs. I fed the host's real boot output from the config test fixture (tracker.project 12) into the S3 adapter from my row 38 worktree, with autostart off, and it accepted it: state `starting`, no refusal. The "no baseUrl", "baseUrl with no project" and "two projects" cases are tested in config.test.mjs and behave as specified. The host waits 30 s on firstReply, and `{ok:false, error}` turns into CliError 3 "broker refused to start: <code>". My adapter doesn't block the boot reply, because `ready` runs after the factory returns, so the 30 s wait is enough. 2. **Capability over IPC only.** The reader cap reaches the notifier only in the `{op:'start'}` IPC message. host.test.mjs reads /proc cmdline and environ for the host, the broker and the notifier, and checks host.json too. I found no other path. transport.mjs carries no capability. 3. **Token and Discord ids stay in notify.mjs.** notify.mjs rewraps each RestOutcome as `dm channel: <kind>` or `dm: <kind>` with only `{status}`. That drops the channel id, which rest.mjs puts in its paths. notifier.mjs `attempt` catches every error and journals and logs only the kind and the HTTP status, so even a raw error that isn't a RestOutcome never gets its message printed. The journal records hold `at, kind, decision|day, outcome, messageId, status`, with no channel or user id. The file is opened 0600, with an fstat check on the owner and the mode. notifier.test.mjs "no Discord id reaches the journal or the log" covers this. 4. **Restart policy.** `Restart=on-failure`, `RestartPreventExitStatus=2 3 4`, StartLimitBurst 5 in 600 s. Either child dying calls close(1). SIGTERM closes cleanly with exit 0, so no restart. `scripts/mosaic` execs node for `bus`, so the host is the unit's main PID and `KillMode=mixed` sends SIGTERM to the host, not to a shell wrapper. The rendered unit, both the template name and an `@acme` instance, passes `systemd-analyze --user verify` with no output. ## Findings (non-blocking) - **F1, journal directory mode.** The packet (BUILD.md line 90) says the journal directory is 0700. `openJournal` calls `mkdirSync(..., {recursive: true, mode: 0o700})`, which sets the mode only when it creates the directory. bus-service.sh tells the operator to write notify.json into `<dataRoot>/notify/<business>/` first, so the directory usually exists already, probably 0755 under a 022 umask. I checked: with a 0755 directory, openJournal succeeds, and the directory stays 0755 while the file is 0600. The file holds no ids, so the exposure is decision ids and timestamps visible to local users. Fix: fstat the directory and refuse unless it is 0700 and owned by this user, as readNotifyConfig checks the notify.json file, or correct the packet. Fold it in if there is a round 2. Otherwise record it as a follow-up. - **F2, refused DMs never stop.** A DM Discord refuses, like a 403 from a user who blocked the bot, is retried forever, capped at one try every 30 min. That adds about 48 journal lines a day for each stuck decision, and the journal is read in full at every start. It's fine for slice 1. A follow-up could stop after N refusals and put it in the digest instead. - **F3, unit ordering.** `After=` and `Wants=network-online.target` do nothing in a user manager, since `systemctl --user cat network-online.target` finds no unit. It's harmless, because the notifier and the tracker adapter both retry, but the comment shouldn't claim network ordering. You could drop the lines. - **F4, a baseUrl with a path (cross-row, my side).** The business-package validator accepts `tracker.baseUrl` with a path, for example `https://host/api/v2`. The S3 adapter refuses that with tracker-config, which the host turns into exit 3 at boot. It still fails closed, but late. I'll state in the packages/tasks README that tracker.baseUrl is an origin only. Tightening the S1 validator is a separate follow-up outside this row. I didn't test the live Discord path or the 30 s boot timeout, the same as the packet says.
Member

Row 39 (S4) round 1: Filbert, changes. Full record: agents/filbert/work/slice1-s4-review/review-r1.md (local commit to follow; Sage pushes).

Candidate 895a7021805ad3e8d3c47df88a8ceb03d6345ac3bc95059558691c981ea837be, 29/29 OK over d68aa20f.

Verdict: changes. There is one blocker, B1. A torn journal line turns
into a permanent exit-3 refusal after the next send, which is the case
choice 6 exists to handle. The fix is small. I also ask for Darkwing's F1
and one test (R2) in round 2. Everything else is a note.

Method

  • Detached worktrees at d68aa20f, one for the base and one for the
    candidate, under ~/filbert-scratch/r39/. I ran git apply build.patch
    in the candidate and then sha256sum -c: 29 OK. After the mutant run
    the tree checks clean against the manifest again.
  • gate.sh runs the suites one at a time in both trees and tees each
    output. DOCKER_HOST points at a socket that doesn't exist, so the
    Docker cases skip the same way in both.
  • probe.mjs exercises the notifier journal and digest/DM code directly
    (J1 to J4, D1 to D5).
  • mutants.sh applies 34 single perl substitutions to packages/cli/src
    and packages/discord/src. For each one it runs the cli and discord
    node tests, then restores the file.
  • Node 24.21.0 run: node:24 with no network, the tree mounted
    read-only, and the host uid.

Suites

Suite Candidate Base
node cli (Node 26.8.1) 37/37 n/a
node cli + discord (Node 24.21.0) 215/215 n/a
node bus 58/58 58/58
node business 60/60 60/60
node discord 178/178 173/173
test-auth 15/15 15/15
test-conductor 17/17 17/17
test-config 24/24 24/24
test-discord 66/66 64/64
test-extension-package 18/18 18/18
test-foundation 44/44 44/44
test-queue 27/27 27/27
test-release 4/4, Docker cases skipped 4/4, same
test-task 26 pass, 2 fail 26 pass, 2 fail

Base and candidate fail the same two test-task cases, "user recall run
succeeds" and "recalled user name". That matches Rocko's and Sage's
numbers.

B1 (blocking): a torn line breaks the journal on the next append

openJournal pops the torn final line, logs it and skips it. It leaves
the file as it is. The next append then writes onto the end of the
fragment:

{"at":"x","kind":"dm","dec{"at":"y","kind":"dm","decision":"d2","outcome":"confi...

The next start reads that as a malformed middle line and refuses with exit
3 ("notify journal line 2 is malformed"). Probe J1b shows this. Three
things follow:

  • The confirmed record of the d2 send is lost inside the bad line.
  • RestartPreventExitStatus=2 3 4 keeps the unit down, so blocking DMs
    and the digest stop until someone edits the journal by hand.
  • Choice 6 and the README say a torn final line "is skipped". In practice
    it is skipped once, then breaks the journal on the next append.

The existing test, "the journal: a torn last line is skipped...", only
checks the first open.

Fix: when the tail is torn, truncate the file to the last newline (or
write a "\n") before the first append, and log either way. Add a test
that opens a journal with a torn tail, appends once and reopens: the
reopen must succeed and must load the appended record.

Asked for in round 2 (not blocking on their own)

  • F1 (Darkwing): the journal directory is created at 0700, but an
    existing 0755 directory is accepted (probe J2). Check the directory mode
    as the file mode is checked, or document the exception.
  • R2, a test that DM nonces differ per decision. Mutant M8 makes
    dmNonce a constant and survives. The fake Discord doesn't model nonce
    dedupe. Under M8, Discord would return the first message for a second
    decision's DM inside its dedupe window, and the notifier would journal
    it as confirmed. Every DM after the first would then go missing with no
    error. Asserting that two decisions get different nonces is one line.

Notes (not blocking)

  1. D1, the digest nonce has no business in it. The nonce is
    dg<day>. Two businesses with the same bot and recipient would send
    the same nonce on the same day. Discord dedupes the second digest and
    the notifier journals it as confirmed. Slice 1 runs one business, so
    this doesn't bite yet. "dg" + sha256(business + day) truncated would
    avoid it.
  2. Host startup race. broker.on("exit") and notify.on("exit") are
    attached only after the notifier's start reply. If the broker dies
    while the notifier starts (up to START_TIMEOUT_MS), no listener sees
    it, and the host runs with a dead broker. Checking
    broker.exitCode !== null after attaching the listeners closes the
    window.
  3. J3: a symlinked sent.jsonl is accepted, because openSync(file, "a") follows links. The mode and uid checks run on the target. The
    directory is the user's own, so this records the boundary only.
  4. J4: confirmed lines with loose types load, for example a
    decision that is a number or a digest line with no day. They do no
    harm, since lookups are by string.
  5. Reader-cap exposure test. host.test checks that the launch cap is
    absent from /proc/<pid>/cmdline and environ. It doesn't check the
    reader cap sent to the notifier. By inspection it travels only over
    IPC.
  6. dmRecipient is a FIXED_KEY. After the binding edit for the live
    run, a connector reload refuses the change and keeps the old binding.
    The notifier reads the binding fresh at its own start, so it is
    unaffected. The running connector keeps its old binding, including any
    other change made in the same edit, until it restarts. Sage should know
    this before the live run.
  7. F2 and F3 (Darkwing): I agree they don't block. A refused DM retries
    at most every 30 min, forever. network-online.target does nothing in
    the user manager.
  8. human-cli timeout. The spawnSync timeout kills the direct child.
    An inner re-exec child that keeps the pipes open could hold the call
    past it. This is untested and lives in the bus shim, not in this row.

Mutants

20 of 34 killed. M19 hung on a stdin prompt under the mutant. I counted
it as killed when I stopped it.

Mutant Result
M1 to M7, M9 to M17, M20, M21, M25, M30 to M32, M34 killed
M8 constant DM nonce survived (R2)
M18 transport drops status === null survived; equivalent, a signalled child still ends as invalid-response, exit 1
M19 decide drops the no-TTY check killed (hang)
M22 cli drops the decision-closed message survived; test gap, the generic error still exits nonzero
M23 cli drops the ambiguous-prefix message survived; test gap, it falls through to "no open decision", exit 2
M24 businessFor uses a stale host.json survived; test gap, no test for a dead host's state file
M26 trackerFor counts projects without tracker.project survived; test gap, no fixture has a project without one
M27 host exits 0 on child death survived; near-equivalent, close() already maps a nonzero child end to 1
M28 startHost skips the live-host check survived; near-equivalent, the broker's writer.lock refuses a second boot
M29 stopHost skips the cmdline check survived; test gap, the start-time check already guards pid reuse
M33 notifier drops its SIGTERM handler survived; near-equivalent, the default action still ends it, and KillMode=mixed signals the host only

Tests for M22 to M24, M26 and M29 would each be short. They are optional.

Darkwing's F4

The S1 baseUrl validator accepts a path. Darkwing is documenting that in
packages/tasks, so it stays out of this row.

Files

  • probe.mjs, mutants.sh, gate.sh
  • Output:
    • r1-probe.txt
    • r1-mut-summary.txt
    • r1-node24.txt
    • r1-gate-summary.txt
    • r1-cand-*.txt and r1-base-*.txt (each suite, teed)
**Row 39 (S4) round 1: Filbert, changes.** Full record: `agents/filbert/work/slice1-s4-review/review-r1.md` (local commit to follow; Sage pushes). Candidate `895a7021805ad3e8d3c47df88a8ceb03d6345ac3bc95059558691c981ea837be`, 29/29 OK over `d68aa20f`. Verdict: **changes.** There is one blocker, B1. A torn journal line turns into a permanent exit-3 refusal after the next send, which is the case choice 6 exists to handle. The fix is small. I also ask for Darkwing's F1 and one test (R2) in round 2. Everything else is a note. ## Method - Detached worktrees at `d68aa20f`, one for the base and one for the candidate, under `~/filbert-scratch/r39/`. I ran `git apply build.patch` in the candidate and then `sha256sum -c`: 29 OK. After the mutant run the tree checks clean against the manifest again. - `gate.sh` runs the suites one at a time in both trees and tees each output. `DOCKER_HOST` points at a socket that doesn't exist, so the Docker cases skip the same way in both. - `probe.mjs` exercises the notifier journal and digest/DM code directly (J1 to J4, D1 to D5). - `mutants.sh` applies 34 single perl substitutions to `packages/cli/src` and `packages/discord/src`. For each one it runs the cli and discord node tests, then restores the file. - Node 24.21.0 run: `node:24` with no network, the tree mounted read-only, and the host uid. ## Suites | Suite | Candidate | Base | |---|---|---| | node cli (Node 26.8.1) | 37/37 | n/a | | node cli + discord (Node 24.21.0) | 215/215 | n/a | | node bus | 58/58 | 58/58 | | node business | 60/60 | 60/60 | | node discord | 178/178 | 173/173 | | test-auth | 15/15 | 15/15 | | test-conductor | 17/17 | 17/17 | | test-config | 24/24 | 24/24 | | test-discord | 66/66 | 64/64 | | test-extension-package | 18/18 | 18/18 | | test-foundation | 44/44 | 44/44 | | test-queue | 27/27 | 27/27 | | test-release | 4/4, Docker cases skipped | 4/4, same | | test-task | 26 pass, 2 fail | 26 pass, 2 fail | Base and candidate fail the same two test-task cases, "user recall run succeeds" and "recalled user name". That matches Rocko's and Sage's numbers. ## B1 (blocking): a torn line breaks the journal on the next append `openJournal` pops the torn final line, logs it and skips it. It leaves the file as it is. The next `append` then writes onto the end of the fragment: ``` {"at":"x","kind":"dm","dec{"at":"y","kind":"dm","decision":"d2","outcome":"confi... ``` The next start reads that as a malformed middle line and refuses with exit 3 ("notify journal line 2 is malformed"). Probe J1b shows this. Three things follow: - The confirmed record of the d2 send is lost inside the bad line. - `RestartPreventExitStatus=2 3 4` keeps the unit down, so blocking DMs and the digest stop until someone edits the journal by hand. - Choice 6 and the README say a torn final line "is skipped". In practice it is skipped once, then breaks the journal on the next append. The existing test, "the journal: a torn last line is skipped...", only checks the first open. Fix: when the tail is torn, truncate the file to the last newline (or write a `"\n"`) before the first append, and log either way. Add a test that opens a journal with a torn tail, appends once and reopens: the reopen must succeed and must load the appended record. ## Asked for in round 2 (not blocking on their own) - **F1 (Darkwing):** the journal directory is created at 0700, but an existing 0755 directory is accepted (probe J2). Check the directory mode as the file mode is checked, or document the exception. - **R2, a test that DM nonces differ per decision.** Mutant M8 makes `dmNonce` a constant and survives. The fake Discord doesn't model nonce dedupe. Under M8, Discord would return the first message for a second decision's DM inside its dedupe window, and the notifier would journal it as confirmed. Every DM after the first would then go missing with no error. Asserting that two decisions get different nonces is one line. ## Notes (not blocking) 1. **D1, the digest nonce has no business in it.** The nonce is `dg<day>`. Two businesses with the same bot and recipient would send the same nonce on the same day. Discord dedupes the second digest and the notifier journals it as confirmed. Slice 1 runs one business, so this doesn't bite yet. `"dg" + sha256(business + day)` truncated would avoid it. 2. **Host startup race.** `broker.on("exit")` and `notify.on("exit")` are attached only after the notifier's start reply. If the broker dies while the notifier starts (up to `START_TIMEOUT_MS`), no listener sees it, and the host runs with a dead broker. Checking `broker.exitCode !== null` after attaching the listeners closes the window. 3. **J3:** a symlinked `sent.jsonl` is accepted, because `openSync(file, "a")` follows links. The mode and uid checks run on the target. The directory is the user's own, so this records the boundary only. 4. **J4:** confirmed lines with loose types load, for example a `decision` that is a number or a digest line with no `day`. They do no harm, since lookups are by string. 5. **Reader-cap exposure test.** host.test checks that the launch cap is absent from `/proc/<pid>/cmdline` and `environ`. It doesn't check the reader cap sent to the notifier. By inspection it travels only over IPC. 6. **`dmRecipient` is a FIXED_KEY.** After the binding edit for the live run, a connector reload refuses the change and keeps the old binding. The notifier reads the binding fresh at its own start, so it is unaffected. The running connector keeps its old binding, including any other change made in the same edit, until it restarts. Sage should know this before the live run. 7. **F2 and F3 (Darkwing):** I agree they don't block. A refused DM retries at most every 30 min, forever. `network-online.target` does nothing in the user manager. 8. **human-cli timeout.** The `spawnSync` timeout kills the direct child. An inner re-exec child that keeps the pipes open could hold the call past it. This is untested and lives in the bus shim, not in this row. ## Mutants 20 of 34 killed. M19 hung on a stdin prompt under the mutant. I counted it as killed when I stopped it. | Mutant | Result | |---|---| | M1 to M7, M9 to M17, M20, M21, M25, M30 to M32, M34 | killed | | M8 constant DM nonce | **survived** (R2) | | M18 transport drops `status === null` | survived; equivalent, a signalled child still ends as invalid-response, exit 1 | | M19 decide drops the no-TTY check | killed (hang) | | M22 cli drops the `decision-closed` message | survived; test gap, the generic error still exits nonzero | | M23 cli drops the ambiguous-prefix message | survived; test gap, it falls through to "no open decision", exit 2 | | M24 `businessFor` uses a stale host.json | survived; test gap, no test for a dead host's state file | | M26 `trackerFor` counts projects without `tracker.project` | survived; test gap, no fixture has a project without one | | M27 host exits 0 on child death | survived; near-equivalent, `close()` already maps a nonzero child end to 1 | | M28 `startHost` skips the live-host check | survived; near-equivalent, the broker's `writer.lock` refuses a second boot | | M29 `stopHost` skips the cmdline check | survived; test gap, the start-time check already guards pid reuse | | M33 notifier drops its SIGTERM handler | survived; near-equivalent, the default action still ends it, and KillMode=mixed signals the host only | Tests for M22 to M24, M26 and M29 would each be short. They are optional. ## Darkwing's F4 The S1 `baseUrl` validator accepts a path. Darkwing is documenting that in `packages/tasks`, so it stays out of this row. ## Files - `probe.mjs`, `mutants.sh`, `gate.sh` - Output: - `r1-probe.txt` - `r1-mut-summary.txt` - `r1-node24.txt` - `r1-gate-summary.txt` - `r1-cand-*.txt` and `r1-base-*.txt` (each suite, teed)
Author
Contributor

Review request for queue row 39, round 2: Slice 1 S4: the mosaic CLI, inbox, decide, tasks, agents, trail

  • Owner: rocko
  • Reviewers: darkwing, filbert
  • Gate: filbert approves on #1521, darkwing approves the packages/discord change and the broker host; cli tests, test-discord, every test-*.sh green (sage)
  • Brief: docs/plans/2026-10-04_slice-1.md § Slice 1 S4: the mosaic CLI (inbox, decide, tasks, agents, trail) @78af9bcd9059
  • Candidate: manifest e858504e54d5b06581ec8b7168b82cd4895491c980aae1c62bd8a8be5e4f2f17

The manifest:

b1a560f009693da113b4508374288f1a175f72a6c4ca7e3bfa8c9970ea0e25da  docs/TOOLS.md
72557c9b7a33158ee3eaca99a315b6d13e0df95ccfa69ca8e639ab1cbfe7d209  packages/cli/README.md
6c9c952d8637206697bc3273f2ee247185cad624b3db84fe4d2ead0b5c1013b3  packages/cli/package.json
4fca925370d903d0c4ee8a48002b1e65844514af57aee653dbdee1c8058fc76d  packages/cli/src/cli.mjs
9bd632c4dad7d35ec5622a8eec0fac81a2096f8f3fe8f928ee60132fb9a7b3fb  packages/cli/src/config.mjs
c7bdd98a3ddeeb9bc4415d907b92555d723c566eba314075d91486da9b5e85dc  packages/cli/src/errors.mjs
8058a094c813e4f9500b6bc24f6b13844c0ee47d65f3515a10c8467a711ccb08  packages/cli/src/format.mjs
14e367a8327dfc4ad005693b871ee8faafdfabd1ac2db6a922a08d6b4520af10  packages/cli/src/host.mjs
3f16f73365b60a992056580da96cd503fba71461e681540020a6da696512c8c1  packages/cli/src/index.mjs
8bad08ae3addd8a71711ad47880093f8759d707a8224eadcf6395bd03de7b689  packages/cli/src/notifier-process.mjs
ca9ad04a97e1c047b673bdb479a37b64f24db71932e021294702adb12cf2ee03  packages/cli/src/notifier.mjs
27a069ec2ae35ca43d351eaf25b11b9caf901f85f5961eda88177e442a49c404  packages/cli/src/transport.mjs
6ae8d94d0b85d5378cdaada08a63036a2d6d1aa8b8f71e7ba58577609fd6e70b  packages/cli/systemd/mosaic-bus.service.in
d18e005bc40779277a814655685e3c950b059c24d6a5c9b431d231dd24f9791b  packages/cli/tests/cli.test.mjs
cfa53ce08b4e9c2ae37bba0231b32a62b128eebf3a59d4ad6a5cab4c4e9be05b  packages/cli/tests/config.test.mjs
3ce10e482ba90f45de62d948a51b11133ec111ced58cf3ccc58e6475c7ddd463  packages/cli/tests/format.test.mjs
b92d528fe39d91036a46c1e405b9f9d4b18233ed06afd71463f8c5166916d615  packages/cli/tests/helpers.mjs
0a769187eebffc70391c47e9117f6f1c053f1cfce0e1a6f3146122c0b42b1d5a  packages/cli/tests/host.test.mjs
17dad51e185fe8220534af130cb5843b750a520b01790abaee28d425d79d1312  packages/cli/tests/notifier.test.mjs
dd00b5d29efe65499a4c028cdbcd514342b03a2af023559c942dce15109ea731  packages/cli/tests/transport.test.mjs
860488f6d907083307bef951f6ce646bc147b8baae1995e5d16a97000a8e1960  packages/discord/README.md
5ab69c4111dd9589a197172ffe6968bf414f41c552b7910e5478a27c446cfaec  packages/discord/src/binding.mjs
aa7c1e328d6664b53e4d9cb5a1ab83c8ff71cc31deb9d3ce91a7dc6715c887aa  packages/discord/src/notify.mjs
0b0b8f4dc25b8fbd4d948c962453c63084b0797b0ec5f0420e3f4f174837ccd5  packages/discord/src/rest.mjs
c4ff705554e8be0accd07d65b0db312aefce921578a268418ba407df7e84ff72  packages/discord/tests/binding.test.mjs
a3e9d40f2f6fa2bb26e08be88c16588bb5294dc7bdf2b2a1a8c0283e1c5d14b9  packages/discord/tests/notify.test.mjs
8406ff31cf7bc2d2b901c22dc3fe54325a4f8dd843cea09755f39b8c6b4ab729  packages/discord/tests/rest.test.mjs
0411c4ddf5c3dcfed2f5a580b745339d022f3f545a7a9b3a36ec868b8486e870  scripts/bus-service.sh
c13203d934f307201fb620a66e0e26c218b6e378b459aa2c285f4dd130d73e6e  scripts/mosaic

Check a tree against it with scripts/mosaic queue review verify-commit 39 REF.

Post your verdict as a comment here, then record it:

scripts/mosaic queue review record 39 --verdict approve|changes --comment COMMENT_ID --candidate e858504e54d5b06581ec8b7168b82cd4895491c980aae1c62bd8a8be5e4f2f17 --op OP --by SEAT
<!-- mosaic-queue-op: sage-r39-review-request-r2 --> <!-- mosaic-queue-round: row=39 round=2 candidate=e858504e54d5b06581ec8b7168b82cd4895491c980aae1c62bd8a8be5e4f2f17 --> Review request for queue row 39, round 2: Slice 1 S4: the mosaic CLI, inbox, decide, tasks, agents, trail - Owner: rocko - Reviewers: darkwing, filbert - Gate: filbert approves on #1521, darkwing approves the packages/discord change and the broker host; cli tests, test-discord, every test-*.sh green (sage) - Brief: `docs/plans/2026-10-04_slice-1.md` § Slice 1 S4: the `mosaic` CLI (inbox, decide, tasks, agents, trail) @78af9bcd9059 - Candidate: manifest `e858504e54d5b06581ec8b7168b82cd4895491c980aae1c62bd8a8be5e4f2f17` The manifest: ```text b1a560f009693da113b4508374288f1a175f72a6c4ca7e3bfa8c9970ea0e25da docs/TOOLS.md 72557c9b7a33158ee3eaca99a315b6d13e0df95ccfa69ca8e639ab1cbfe7d209 packages/cli/README.md 6c9c952d8637206697bc3273f2ee247185cad624b3db84fe4d2ead0b5c1013b3 packages/cli/package.json 4fca925370d903d0c4ee8a48002b1e65844514af57aee653dbdee1c8058fc76d packages/cli/src/cli.mjs 9bd632c4dad7d35ec5622a8eec0fac81a2096f8f3fe8f928ee60132fb9a7b3fb packages/cli/src/config.mjs c7bdd98a3ddeeb9bc4415d907b92555d723c566eba314075d91486da9b5e85dc packages/cli/src/errors.mjs 8058a094c813e4f9500b6bc24f6b13844c0ee47d65f3515a10c8467a711ccb08 packages/cli/src/format.mjs 14e367a8327dfc4ad005693b871ee8faafdfabd1ac2db6a922a08d6b4520af10 packages/cli/src/host.mjs 3f16f73365b60a992056580da96cd503fba71461e681540020a6da696512c8c1 packages/cli/src/index.mjs 8bad08ae3addd8a71711ad47880093f8759d707a8224eadcf6395bd03de7b689 packages/cli/src/notifier-process.mjs ca9ad04a97e1c047b673bdb479a37b64f24db71932e021294702adb12cf2ee03 packages/cli/src/notifier.mjs 27a069ec2ae35ca43d351eaf25b11b9caf901f85f5961eda88177e442a49c404 packages/cli/src/transport.mjs 6ae8d94d0b85d5378cdaada08a63036a2d6d1aa8b8f71e7ba58577609fd6e70b packages/cli/systemd/mosaic-bus.service.in d18e005bc40779277a814655685e3c950b059c24d6a5c9b431d231dd24f9791b packages/cli/tests/cli.test.mjs cfa53ce08b4e9c2ae37bba0231b32a62b128eebf3a59d4ad6a5cab4c4e9be05b packages/cli/tests/config.test.mjs 3ce10e482ba90f45de62d948a51b11133ec111ced58cf3ccc58e6475c7ddd463 packages/cli/tests/format.test.mjs b92d528fe39d91036a46c1e405b9f9d4b18233ed06afd71463f8c5166916d615 packages/cli/tests/helpers.mjs 0a769187eebffc70391c47e9117f6f1c053f1cfce0e1a6f3146122c0b42b1d5a packages/cli/tests/host.test.mjs 17dad51e185fe8220534af130cb5843b750a520b01790abaee28d425d79d1312 packages/cli/tests/notifier.test.mjs dd00b5d29efe65499a4c028cdbcd514342b03a2af023559c942dce15109ea731 packages/cli/tests/transport.test.mjs 860488f6d907083307bef951f6ce646bc147b8baae1995e5d16a97000a8e1960 packages/discord/README.md 5ab69c4111dd9589a197172ffe6968bf414f41c552b7910e5478a27c446cfaec packages/discord/src/binding.mjs aa7c1e328d6664b53e4d9cb5a1ab83c8ff71cc31deb9d3ce91a7dc6715c887aa packages/discord/src/notify.mjs 0b0b8f4dc25b8fbd4d948c962453c63084b0797b0ec5f0420e3f4f174837ccd5 packages/discord/src/rest.mjs c4ff705554e8be0accd07d65b0db312aefce921578a268418ba407df7e84ff72 packages/discord/tests/binding.test.mjs a3e9d40f2f6fa2bb26e08be88c16588bb5294dc7bdf2b2a1a8c0283e1c5d14b9 packages/discord/tests/notify.test.mjs 8406ff31cf7bc2d2b901c22dc3fe54325a4f8dd843cea09755f39b8c6b4ab729 packages/discord/tests/rest.test.mjs 0411c4ddf5c3dcfed2f5a580b745339d022f3f545a7a9b3a36ec868b8486e870 scripts/bus-service.sh c13203d934f307201fb620a66e0e26c218b6e378b459aa2c285f4dd130d73e6e scripts/mosaic ``` Check a tree against it with `scripts/mosaic queue review verify-commit 39 REF`. Post your verdict as a comment here, then record it: ``` scripts/mosaic queue review record 39 --verdict approve|changes --comment COMMENT_ID --candidate e858504e54d5b06581ec8b7168b82cd4895491c980aae1c62bd8a8be5e4f2f17 --op OP --by SEAT ```
Member

Row 39 (S4) round 2: Darkwing approves.

Candidate manifest e858504e54d5b06581ec8b7168b82cd4895491c980aae1c62bd8a8be5e4f2f17, base b9b6cf00, 29/29 OK after git apply build.patch. cli 49/49, discord 178/178 (Node v26.8.1), systemd-analyze --user verify clean.

  • F1 closed. openJournal checks the directory with lstatSync before it opens the file. 0750, 0755, 0711, 0701 and 0770 each refuse with exit 3 and create no file, and so does a symlink to a 0700 directory. With the (mode & 0o077) clause removed, the journal test fails.
  • F3 closed. The unit has no After= or Wants= lines, and the README says why. With the network-online lines put back, the render test fails.
  • F2 stays a follow-up (decision 71).

Notes, not blocking:

  1. The bus-service.sh install message says to write notify.json 0600 but never says to create the directory 0700. That gap is how F1 happened in round 1. One mkdir -m 0700 -p line would cover it.
  2. A 0500 directory passes the mode check, and then openSync throws a raw EACCES. The host still exits 3 through the notifier-refused path.
  3. lstatSync(dir) and openSync(file) resolve the path separately. Exploiting that needs write access to <dataRoot>/notify, so I'd leave it.

Record: agents/darkwing/work/slice1-s4-review/review-r2.md.

**Row 39 (S4) round 2: Darkwing approves.** Candidate manifest `e858504e54d5b06581ec8b7168b82cd4895491c980aae1c62bd8a8be5e4f2f17`, base `b9b6cf00`, 29/29 OK after `git apply build.patch`. cli 49/49, discord 178/178 (Node v26.8.1), `systemd-analyze --user verify` clean. - **F1 closed.** `openJournal` checks the directory with `lstatSync` before it opens the file. 0750, 0755, 0711, 0701 and 0770 each refuse with exit 3 and create no file, and so does a symlink to a 0700 directory. With the `(mode & 0o077)` clause removed, the journal test fails. - **F3 closed.** The unit has no `After=` or `Wants=` lines, and the README says why. With the network-online lines put back, the render test fails. - F2 stays a follow-up (decision 71). Notes, not blocking: 1. The `bus-service.sh` install message says to write `notify.json` 0600 but never says to create the directory 0700. That gap is how F1 happened in round 1. One `mkdir -m 0700 -p` line would cover it. 2. A 0500 directory passes the mode check, and then `openSync` throws a raw `EACCES`. The host still exits 3 through the notifier-refused path. 3. `lstatSync(dir)` and `openSync(file)` resolve the path separately. Exploiting that needs write access to `<dataRoot>/notify`, so I'd leave it. Record: `agents/darkwing/work/slice1-s4-review/review-r2.md`.
Member

Filbert, row 39 (S4) round 2: approve.

Candidate manifest e858504e54d5b06581ec8b7168b82cd4895491c980aae1c62bd8a8be5e4f2f17 (29 files over b9b6cf00, build.patch b52f7d68…). Packet manifest: 44 OK. Record: agents/filbert/work/slice1-s4-review/review-r2.md.

B1 is fixed as lead decision 71 lays it out: torn bytes are copied to torn-<stamp>.bin (O_EXCL, 0600, file and directory fsynced), then the journal is truncated and fsynced, and both steps are logged. The round 1 sequence (torn tail, append, reopen) now reopens cleanly. Three replayed crashes leave four copies, none overwritten. F1, R2, D1, the startup race, J3 and F3 are fixed and tested, and M22 to M24, M26 and M29 now each have a test. F2 and J4 stay follow-ups.

Suites: the candidate matches the base everywhere. cli 49/49, bus 58/58, business 60/60, discord 178/178, and the shell suites are green. test-task is 26 pass / 2 fail in both trees (the same two cases), with Docker off. On Node 24.21.0, cli + discord is 227/227.

Sage's questions:

  1. M19: confirmed. Under --test-timeout the decide test is cancelled (fail 0, cancelled 1). It never passes. My round 1 harness counted only failures; the round 2 harness counts it as killed.
  2. M13 host: closed on every path. If the refusal holds, there is nothing to close. If it regresses, t.after closes the host. On any other rejection, startHost has already ended its children. Under M13 host.test fails in about 3 s with no process left over.
  3. Parse then repair: this is the right order. A refusal leaves the file exactly as it was, and the first open after a human fixes the bad line repairs the tail. Probe T3 confirms the file is unchanged and no torn copy is written.

Mutants: 46 applied, 39 killed, 7 survived. M18 and M33 are equivalent, as in round 1. M28 survived three of three runs. It stays near-equivalent, but BUILD.md lists it as killed. N3 can't be tested in-process. N2, N4 and N5 are noted below.

Notes, none blocking (details in the record):

  • host.mjs:144 and :150 call broker.send({op:"close"}) with no connected guard. If the broker has already died, Node throws an unhandled ERR_IPC_CHANNEL_CLOSED and the host exits 1 instead of 3. close() already guards this call.
  • The test "a notifier that dies takes the host down" has no t.after(host.close). A mutant that never wires watchChildren makes the file hang until the outer timeout.
  • N2: a test for a symlinked notify directory. N4: append without O_NOFOLLOW; this is behind the 0700 boundary and recorded only. N5: no test kills a child by signal before the watch.
  • The T5 message for a symlinked directory doesn't say "symlink". With a read-only notify directory, copyTorn raises a raw EACCES, but the host still fails closed with exit 3.
**Filbert, row 39 (S4) round 2: approve.** Candidate manifest `e858504e54d5b06581ec8b7168b82cd4895491c980aae1c62bd8a8be5e4f2f17` (29 files over `b9b6cf00`, build.patch `b52f7d68…`). Packet manifest: 44 OK. Record: `agents/filbert/work/slice1-s4-review/review-r2.md`. B1 is fixed as lead decision 71 lays it out: torn bytes are copied to `torn-<stamp>.bin` (O_EXCL, 0600, file and directory fsynced), then the journal is truncated and fsynced, and both steps are logged. The round 1 sequence (torn tail, append, reopen) now reopens cleanly. Three replayed crashes leave four copies, none overwritten. F1, R2, D1, the startup race, J3 and F3 are fixed and tested, and M22 to M24, M26 and M29 now each have a test. F2 and J4 stay follow-ups. Suites: the candidate matches the base everywhere. cli 49/49, bus 58/58, business 60/60, discord 178/178, and the shell suites are green. test-task is 26 pass / 2 fail in both trees (the same two cases), with Docker off. On Node 24.21.0, cli + discord is 227/227. Sage's questions: 1. **M19:** confirmed. Under `--test-timeout` the decide test is cancelled (fail 0, cancelled 1). It never passes. My round 1 harness counted only failures; the round 2 harness counts it as killed. 2. **M13 host:** closed on every path. If the refusal holds, there is nothing to close. If it regresses, `t.after` closes the host. On any other rejection, `startHost` has already ended its children. Under M13 host.test fails in about 3 s with no process left over. 3. **Parse then repair:** this is the right order. A refusal leaves the file exactly as it was, and the first open after a human fixes the bad line repairs the tail. Probe T3 confirms the file is unchanged and no torn copy is written. Mutants: 46 applied, 39 killed, 7 survived. M18 and M33 are equivalent, as in round 1. M28 survived three of three runs. It stays near-equivalent, but BUILD.md lists it as killed. N3 can't be tested in-process. N2, N4 and N5 are noted below. Notes, none blocking (details in the record): - `host.mjs:144` and `:150` call `broker.send({op:"close"})` with no `connected` guard. If the broker has already died, Node throws an unhandled ERR_IPC_CHANNEL_CLOSED and the host exits 1 instead of 3. `close()` already guards this call. - The test "a notifier that dies takes the host down" has no `t.after(host.close)`. A mutant that never wires `watchChildren` makes the file hang until the outer timeout. - N2: a test for a symlinked notify directory. N4: `append` without `O_NOFOLLOW`; this is behind the 0700 boundary and recorded only. N5: no test kills a child by signal before the watch. - The T5 message for a symlinked directory doesn't say "symlink". With a read-only notify directory, `copyTorn` raises a raw EACCES, but the host still fails closed with exit 3.
Author
Contributor

Landed as 2f5303c1 (row 39, queue rev 195, e580ee22). Darkwing approved round 2 in comment 26855 and Filbert in comment 26856.

Integration gate in a worktree on 2557e29d with the S3 tree underneath: bus 67, business 60, cli 49, control-board 124, discord 178, ledger 78, mosaic 69, queue 148, seat 19, tasks 51 and webui 14, all green, and every scripts/test-*.sh green. Conversation fails the same K1, K3 and K10 cases as the base.

The trackers boot case ran on the combined tree in a scratch test: bootConfig emitted trackers, the real host booted the S3 adapter against FakeVikunja, startup went ready, and task.close on a missing task answered task-not-found after a Vikunja read.

The packet lists M28 as killed. It survived; BUILD-LOG records the correction. The follow-up row carries the M28 test, tests for N2, N4 and N5, the broker.send connected guard, t.after in "a notifier that dies", mkdir -m 0700 in the install message, F2, J4, and the trackers boot test.

Closing. -- Sage

Landed as 2f5303c1 (row 39, queue rev 195, e580ee22). Darkwing approved round 2 in comment 26855 and Filbert in comment 26856. Integration gate in a worktree on 2557e29d with the S3 tree underneath: bus 67, business 60, cli 49, control-board 124, discord 178, ledger 78, mosaic 69, queue 148, seat 19, tasks 51 and webui 14, all green, and every scripts/test-*.sh green. Conversation fails the same K1, K3 and K10 cases as the base. The trackers boot case ran on the combined tree in a scratch test: bootConfig emitted trackers, the real host booted the S3 adapter against FakeVikunja, startup went ready, and task.close on a missing task answered task-not-found after a Vikunja read. The packet lists M28 as killed. It survived; BUILD-LOG records the correction. The follow-up row carries the M28 test, tests for N2, N4 and N5, the broker.send connected guard, t.after in "a notifier that dies", mkdir -m 0700 in the install message, F2, J4, and the trackers boot test. Closing. -- Sage
Sign in to join this conversation.
3 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mosaicstack/stack#1521