From a731e09b1c2221f436569da3a8940f794d302b68 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Mon, 12 Oct 2026 13:54:11 -0500 Subject: [PATCH] plans: one-copy brief for bus, Console and mosaic CLI (#225, Mos sage-0094/0095) Co-Authored-By: Claude Opus 5.5 --- docs/plans/2026-10-12_one-copy-stack.md | 182 ++++++++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 docs/plans/2026-10-12_one-copy-stack.md diff --git a/docs/plans/2026-10-12_one-copy-stack.md b/docs/plans/2026-10-12_one-copy-stack.md new file mode 100644 index 000000000..c5494e715 --- /dev/null +++ b/docs/plans/2026-10-12_one-copy-stack.md @@ -0,0 +1,182 @@ +# Bus, Console and mosaic CLI on one pinned copy (2026-10-12) + +Status: written by Sage, lead. It follows `docs/plans/BRIEF-TEMPLATE.md`. +It is the fix plan in my comment 52 on Vikunja #225, which Mos asked for +in sage-0094 ("Tell me the plan before anything touches the wrapper or +the unit") and sage-0095 ("Keep the one-copy row brief going"). It +follows queue row 77 (#1571), whose round 2 makes `webui-service.sh +install --copy` refuse unless the bus runs from the same copy. + +## Bus, Console and mosaic CLI on one pinned copy (#225) + +### Problem + +On 2026-10-12 Jason ran `mosaic talk pm "Can you hear me now?"` and got +`mosaic: human-required` (#225). The bus accepts a human proof only from +its own `packages/bus/src/human-cli.mjs`: `verifyHuman` compares the +proof process's `argv[1]` with `resolve(cliPath)` +(`packages/bus/src/human.mjs:52-102`), and `cliPath` comes from the bus's +own tree (`packages/bus/src/runtime.mjs:68`). The CLI spawns the +human-cli of its own tree (`packages/cli/src/transport.mjs:12`), and so +does the Console (`packages/webui/src/bus.mjs`). Today three things run +from three trees: + +- the bus unit, `ExecStart=/mnt/storage/src/mosaic-stack/scripts/mosaic + bus start %i`, rendered by `scripts/bus-service.sh` from + `packages/cli/systemd/mosaic-bus.service.in` with REPO set to the + script's own checkout; +- the `mosaic` wrapper (Q81, `~/.npm-global/bin/mosaic`), which runs a + linked worktree at `~/.local/share/mosaic-cli/stack`, pinned at + d3ca2c22, six bus and CLI commits behind; +- the Console, which runs from the checkout today and from a row 77 copy + under `~/.local/share/mosaic-console//` once row 77 lands and is + installed. + +So the proof works only from one tree at a time. The stopgap is the full +path, `/mnt/storage/src/mosaic-stack/scripts/mosaic talk pm ...`, which +Jason confirmed works (6b5b7aef). The bus also runs code from the moving +checkout, so every landing in `packages/bus` or `packages/cli` is live at +its next restart, the same problem row 77 fixes for the Console (lead +decision 109). And the unit starts the host without `--pm` +(`packages/cli/src/cli.mjs:265-279`), so no PM session runs unless +someone starts the host by hand (the S7 run log, 2026-10-12T18:49Z). + +Accepting a second path in the proof (#225 option B) is rejected: it adds +a trusted path and keeps the three trees on different code. + +### Owner and reviewer + +Owner: Rocko. Reviewers: Darkwing and Filbert. Rocko has no Gitea token; +Sage runs the review request as jarvis. This row starts when row 77 is +done, and it goes ahead of row 84, since S7 comes before M1 (Q97). + +### Files owned + +- `scripts/stack-copy.sh`, new +- `scripts/webui-service.sh`, only the `copy` verb, the copies directory + and the same-copy check row 77 round 2 adds +- `scripts/bus-service.sh` +- `packages/cli/systemd/mosaic-bus.service.in` +- tests for the above, under `packages/cli/tests/` and + `packages/webui/tests/`, or a new `scripts/test-stack-copy.sh` +- `packages/cli/README.md`, the "Bus host" section +- `packages/webui/README.md`, the service section +- `docs/TOOLS.md`, the entries for the three scripts only +- `agents/rocko/work/queue-/` + +### What ships + +One copy for all three: + +- `scripts/stack-copy.sh copy COMMIT [--copies DIR]` makes the copy. It + takes over row 77's copy code and rules: canonical checkout only (the + `--allow-worktree` test override stays), the full 40-character id, a + temp directory beside the target and a rename, write permission + removed, and an existing copy left as it is (`exists:`). COMMIT must be + reachable from both `refs/heads/refactor` and + `refs/remotes/origin/refactor`, since the wrapper runs pushed code only + (Q81). +- The default directory is + `${XDG_DATA_HOME:-$HOME/.local/share}/mosaic-stack/copies//`. + `webui-service.sh copy` stays as a call into `stack-copy.sh copy`, so + row 77's documented steps still work, and both scripts read the same + directory. +- The path list grows to what the bus host, the CLI and the Console load: + row 77's list, plus `scripts/mosaic`, `adapters/`, `packages/queue/src` + if any copied path imports it, `package.json` and `package-lock.json`. + The script runs `npm ci` in the temp directory before the rename, so + the copy carries the pi pin's `node_modules` and `pi-pin.mjs`'s lock + check passes there (`packages/conversation/src/pi-pin.mjs:52-57`). The + script refuses if `package.json` is missing from the temp directory, so + `npm ci` can never walk up. Rocko derives the final list from the + imports and records how in the packet. +- Tests build a copy from a fixture repository, then from that copy: + `bus start` reaches `bus host up` with a stand-in config, `cli.mjs + serve` answers `/healthz` 200, and `scripts/mosaic talk` gets past the + human proof against a broker started from the same copy. A path missing + from the list fails the suite. + +The proof across copies, as tests (with the agent-environment check +stubbed as `packages/bus/tests/human.test.mjs` already does, never +bypassed in the code): + +- a human-cli from the same copy as the broker is accepted; +- a human-cli from another copy, or from the checkout, is refused with + `human-required`. + +The bus unit: + +- `bus-service.sh render` and `install` take `--copy ID` and `--pm`. + `--copy ID` names an existing copy by its full id, and the template's + `@REPO@` becomes the copy's path, so `WorkingDirectory`, `ExecStart` + and `Documentation` point at it. Without `--copy` the unit renders as + today, so a fresh install from the checkout still works, and `status` + says the unit runs the checkout. +- `--pm` renders `ExecStart=/scripts/mosaic bus start %i --pm`. A + PM refusal at start exits 3, which the unit doesn't retry + (`RestartPreventExitStatus=2 3 4`), so the host stays down and its + journal names the refusal. The README says so. +- `install` refuses while any `mosaic-bus@` instance is active, + activating or deactivating, with row 77's N1 wording (Jason or Mos stops + it first), and with row 77's N4 rule for a failed `systemctl` read. +- `status` prints the copy id the unit runs, read from its + `WorkingDirectory`, whether `--pm` is in its `ExecStart`, and whether + `refactor` points at another commit. It changes nothing. +- The `--claude PATH` pin (lead decision 91) is unchanged and is rendered + next to `--copy` and `--pm`. + +The check and the wrapper: + +- `stack-copy.sh check` prints the tree each of the three runs: the bus + unit's `WorkingDirectory`, the Console unit's `WorkingDirectory`, and + the copy the `mosaic` wrapper execs. It exits 0 only when all three + name the same copy, and 1 otherwise, naming each path. It changes + nothing. A missing unit or wrapper counts as a mismatch, named as + absent. +- `stack-copy.sh wrapper ID` rewrites `~/.npm-global/bin/mosaic` to exec + `/scripts/mosaic`. It keeps the Q81 lease line that sends + `__lease-capability` to `mosaic-old`, and keeps the Q81 marker on line + 2. It refuses unless the current file carries that marker and isn't a + symlink, and it writes a temp file, then renames it. `--bin DIR` points + it at a test directory. Tests cover the refusal on a foreign file and + the lease line. + +The update step, documented once in `docs/TOOLS.md` and pointed to from +both READMEs: `stack-copy.sh copy COMMIT`; stop the Console, then the +bus; `bus-service.sh install --copy ID --pm` with its flags; +`webui-service.sh install --copy ID` with its flags; `stack-copy.sh +wrapper ID`; start the bus, then the Console; `stack-copy.sh check`, +`webui-service.sh status`, `/healthz` and `scripts/mosaic launches list` +showing the PM. Rollback is the same steps with the earlier id. A bus +restart ends the running sessions (`packages/cli/src/launcher.mjs:304-320`), +so the docs say to do it when no PM turn is in flight. The stops, starts, +installs and the wrapper rewrite are Jason's or Mos's (lead decisions 94, +102 and 103, and Mos's sage-0094: nothing touches the wrapper or the +units before the row lands and Mos says go). + +Suites: the cli, bus and webui node suites, and every +`scripts/test-*.sh`. + +### Out of scope + +Running any step of the update on the live system: stopping, starting, +installing or enabling either unit, and rewriting the real wrapper. +Removing the Q81 worktree at `~/.local/share/mosaic-cli/stack` or +`mosaic-old`; that's Mos's or Jason's after the first update. Pruning old +copies. A health-gated activate and rollback like `scripts/release.sh`, +which is D1. Any change to the human proof itself +(`packages/bus/src/human.mjs`). Any change to `packages/queue`; `mosaic +queue` from a copy refuses as it does from the Q81 worktree today, and +queue work stays on the checkout's `scripts/mosaic`. Relaunching a PM +whose session ended while the host runs. + +### Gate + +Darkwing and Filbert approve on the row's issue, each naming the +candidate manifest. The cli, bus and webui node suites and every +`scripts/test-*.sh` pass on Sage's gate rerun. Before the landing Sage +notifies Mos (decision 109); after it, Sage sends Mos the commit and +`scripts/webui-service.sh status`, `scripts/bus-service.sh status +mosaic-stack` and `/healthz`. #225 closes when Mos's first update +step at the landed commit ends with `stack-copy.sh check` exiting 0, and +Jason's plain `mosaic talk pm "..."` printing `sent` and then a reply.