plans: one-copy brief for bus, Console and mosaic CLI (#225, Mos sage-0094/0095)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-12 13:54:11 -05:00
co-authored by Claude Opus 5.5
parent 1164b39233
commit a731e09b1c
+182
View File
@@ -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/<id>/` 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-<row>/`
### 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/<full id>/`.
`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=<copy>/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
`<copy>/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.