`mosaic fleet --help` reads "Manage the local Mosaic tmux fleet" and every
roster the CLI scaffolds sets `transport: tmux`, but neither `tools/install.sh`
nor `tools/_scripts/mosaic-doctor` contained the string "tmux" at all. A
greenfield host therefore came out of the installer able to install a fleet,
start a fleet, and run no seat, with `mosaic fleet ps` as the operator's first
and only signal.
Measured on mosaic-sbx-dev (Debian, no tmux, framework installed): `mosaic-doctor`
reported 11 warnings and not one of them named the reason no seat could launch.
The installer gets a warning, not a `require_cmd` hard failure: tmux is required
by the fleet, not by mosaic. Hosts that install this to run `mosaic claude` and
never scaffold a roster are common, and failing their install over a binary they
do not need would be wrong. The check runs in `--check` mode too — "what is the
state of this host" is the question `--check` is asked.
Both checks read the roster's own `transport:` rather than assuming tmux, so a
host declaring something else is pointed at the binary it actually needs instead
of at the wrong package.
The two implementations are deliberately parallel and each carries a comment
pointing at the other. They are separate because the installer must answer this
before the framework's own scripts are guaranteed to be on disk. One harness
drives BOTH from the shipped text — the functions are extracted from the scripts
by awk rather than copied — so the pair cannot drift silently, and the test
cannot keep passing after the shipped copy changes.
The harness is wired into `test:framework-shell`. Without that it would have
tripped the #1017 enumeration guard as UNENUMERATED, which is the guard doing
its job: a check nothing runs is not a check.
Evidence:
- red: the harness fails against origin/next ("could not extract
fleet_declared_transport"); `grep -ci tmux` on both files at origin/next = 0.
- green on real hosts, all four branches:
- dev (no tmux, no roster) -> WARN naming tmux, points at `mosaic fleet init`
- dev (no tmux, v2 roster) -> WARN naming the roster, points at `mosaic fleet start`
- dev installer --check -> WARN saying start "reports success and no seat comes up"
- canary (tmux present) -> `[OK] Fleet transport available: tmux` under --verbose,
silent by default (pass() is verbose-gated), installer silent
- harness green on node:24-alpine/busybox, the CI base image.
- `bash -n` x3, `pnpm typecheck` 45/45, fleet specs 342 passed,
enumeration guard OK, its self-test OK, prettier clean.
Refs #1240. Upstream of #1237/#1243 and #1241/#1244: a correct fix for either of
those still leaves this host with no live seat.
Comment-only, no behaviour change. Both raised by scooby in the #1229 review
as non-blocking findings worth writing down rather than fixing here.
F-A: the SHASUMS256.txt check gives integrity, not authenticity. TLS to
$NODE_DIST_BASE is the whole trust root, and MOSAIC_NODE_DIST_BASE widens it
to any mirror with no signature backstop. GPG-verifying SHASUMS256.txt.sig is
filed as its own follow-up so it gets its own review.
F-C: the uname map pulls the glibc build, so musl hosts fail — visibly, via
node_is_suitable, not silently.
This reverts 47e90767. I was wrong: the fix is correct about the cause and
makes the outcome worse.
The acceptance run passed everything I set out to check — greenfield canary
1125, --next --yes, no TTY, rc=0, node v22.23.2 + CLI 0.0.50-next.2413 from a
fresh login shell, and both enforcement hooks wired in ~/.claude/settings.json
where before they were stripped. Then `mosaic doctor` on that same host:
[ERROR] Lease-enforcement hooks (mutator-gate.py, receipt-observer-client.py)
are wired in ~/.claude/settings.json, but broker not healthy
(checkBrokerSupervisorHealth() reports unhealthy). Every gated tool call will
fail closed and BRICK this agent (see #869).
So the change takes a greenfield host from 'enforcement quietly off, agent
works' to 'enforcement wired, broker absent, agent bricks on the first gated
tool call'. The pre-existing behaviour reaches the safe state for the wrong
reason; this reaches the unsafe state for the right one. Safe-for-the-wrong-
reason still wins.
The real defect is underneath both, and it is not an ordering bug:
mosaic __link-claude-settings ... -> rc=0 (leaseEnforcementActivatable:
activatable, wire the hooks)
mosaic doctor -> ERROR (checkBrokerSupervisorHealth:
unhealthy, hooks will brick)
Two capability checks, same host, opposite verdicts. And after a complete
install there is no broker supervisor to be healthy: no systemd --user unit
matching lease/broker, nothing under ~/.mosaic but the bootstrapped node, and
no lease or broker script in ~/.config/mosaic/tools/_scripts/. Lease
enforcement cannot be activated on a greenfield host at all, so
leaseEnforcementActivatable() returning true is the thing that is wrong.
Filing that separately. PR #1229 goes back to exactly the four commits scooby
reviewed.
Co-Authored-By: Claude Opus 5 <[email protected]>
The framework's install.sh ends by running mosaic-link-runtime-assets, which
asks the `mosaic` CLI whether lease enforcement can be activated before
deciding whether to wire the #828 hooks into settings.json. Part 1 (framework)
runs before Part 2 (npm CLI), so on a first install there is no CLI to ask. The
script takes its fail-safe branch, prints a four-line ERROR, and writes
settings.json with mutator-gate.py and receipt-observer-client.py stripped out.
Measured on canary 1125, rolled back to greenfield, `--next --yes`, no TTY:
framework template ~/.config/mosaic/runtime/claude/settings.json
mutator-gate.py 1 occurrence
receipt-observer-client.py 1 occurrence
installed ~/.claude/settings.json after a clean rc=0 install
mutator-gate.py wired: False
receipt-observer-client.py wired: False
So enforcement ends up off because of the order the two halves install in, not
because of anything about the host. Falsified by running the same script by
hand once the CLI existed: rc=0, both hooks wired: True. The guard's real
verdict on that host was 'activatable' the whole time.
This adds one more pass after Part 2. The script is idempotent (unchanged files
are skipped), so on an upgrade — CLI already present, first pass already
correct — it is a no-op. It deliberately does not pass
--allow-inactive-enforcement: Part 1 does not either, and a repair pass must
not be more permissive than the pass it corrects.
Co-Authored-By: Claude Opus 5 <[email protected]>
Two defects found by the second unattended greenfield run on canary (VMID 1125,
rolled back to its greenfield snapshot first).
1. ensure_node() exported the Mosaic-managed Node for the installer process and
nothing wrote it down. The install finished rc=0, put $PREFIX/bin in
~/.profile, and the next login shell found `mosaic` and then died on
env: 'node': No such file or directory
The CLI is a Node script, so a CLI on PATH without its runtime is a
successful install that produces a broken command. persist_node_on_path()
now writes the runtime's bin dir to the same profile, from both the
fresh-install and the already-installed-but-not-on-PATH branches.
2. The 'is it already in a shell rc file' guard was a single
`grep -qslF "$dir" "${rc_files[@]}"` over four paths, most of which do
not exist on a clean host. Handing grep a missing file makes the exit status
implementation-defined: GNU grep 3.11 returns 0 when -q already matched an
earlier file, ugrep 7.5 returns 2 for the missing one regardless. On the 2
path the caller reads 'not present yet' and appends another PATH line, so
every re-install grew the profile. Measured: 3 runs produced 3 duplicate
entries; with the fix, 1.
path_entry_exists() now tests each file for existence and greps it on its
own, so the result does not depend on the grep implementation.
The profile-writing body is factored into persist_on_path(), shared by the CLI
prefix and the Node runtime, since both now need identical treatment.
Verified in a scratch $HOME: fresh write, idempotent across three runs, zsh
routes to .zshenv, an unwritable profile warns and survives set -e, and an
already-on-PATH prefix is a no-op that creates no file. Falsified by restoring
the multi-file grep: duplicates return.
tools/install.sh required node and npm and installed neither. Measured on a
snapshot-reverted Debian 13 image with no node, npm or git: the run stopped
at `require_cmd node` with "Required command not found: node", exit 1,
nothing installed, and no indication of how to proceed.
Adds ensure_node() to preflight. It fetches an official Node.js release into
$HOME/.mosaic/node, verifies it against that release's SHASUMS256.txt, and
refuses rather than degrades when the entry is missing or the checksum does
not match. sha256sum on Linux, shasum on macOS. .tar.gz over the smaller
.tar.xz because gzip is universally present and xz is not — a minimal image
is the case this exists to handle.
No-op when a suitable node is already on PATH, so it never fights an
operator's nvm/fnm/distro node. MOSAIC_SKIP_NODE_BOOTSTRAP=1 declines the
download and fails with instructions instead.
Inlined rather than factored into a sibling file because this script is
fetched standalone by curl and has nothing to source.
The three duplicated PATH blocks in tools/install.sh only warned, so an
unattended install finished with rc=0 and left `mosaic: command not found`
— there was no operator to read the advice and act on it. Measured on a
greenfield Debian 13 sandbox: `--next --yes` installed
@mosaicstack/[email protected] successfully and the CLI was still
unreachable.
Replaces all three copies with one ensure_prefix_on_path helper that
appends the export to ~/.profile (~/.zshenv under zsh) and is a no-op when
the prefix is already on PATH or already in a shell profile.
Not ~/.bashrc: Debian's default .bashrc returns early for non-interactive
shells, so a line appended there is unreachable to `bash -lc`, systemd
units and agent seats — the consumers that need the CLI.
--next now prefers a fast npm @next install (CLI + gateway from the Gitea registry) and falls back to source build at next if the dist-tag is unavailable. Registry lane gated to non-dev, non-explicit-ref next installs; CLI/gateway prerelease versions must share a pipeline suffix. Adds tools/install-next-lane.test.sh (wired into CI). PR-event CI 1635 fully green + review-of-record APPROVE (functional install test, head 2fd7cfc3).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
@mosaic/mosaic is now the single package providing both:
- 'mosaic' binary (CLI: yolo, coord, prdy, tui, gateway, etc.)
- 'mosaic-wizard' binary (installation wizard)
Changes:
- Move packages/cli/src/* into packages/mosaic/src/
- Convert dynamic @mosaic/mosaic imports to static relative imports
- Add CLI deps (ink, react, socket.io-client, @mosaic/config) to mosaic
- Add jsx: react-jsx to mosaic's tsconfig
- Exclude packages/cli from workspace (pnpm-workspace.yaml)
- Update install.sh to install @mosaic/mosaic instead of @mosaic/cli
- Bump version to 0.0.17
This eliminates the circular dependency between @mosaic/cli and
@mosaic/mosaic that was blocking the build graph.
The @mosaic scope registry is configured in ~/.npmrc. Passing --registry
on the install command overrides the default registry for ALL packages,
causing non-@mosaic deps like @clack/prompts to 404 against Gitea.