fix(fleet): tell the operator when the fleet transport is missing (#1240) #1245

Merged
fred merged 2 commits from fix/1240-fleet-transport-check into next 2026-08-16 18:12:34 +00:00
Collaborator

Closes #1240.

The bug in one line

grep -ci tmux on tools/install.sh and on tools/_scripts/mosaic-doctor at origin/next returns 0 for both. The fleet runs entirely inside tmux, the installer never installs it, and the audit script never mentions it.

What that costs an operator

Measured on mosaic-sbx-dev — Debian, greenfield, framework installed, no tmux:

[mosaic-doctor] warnings=11

Eleven warnings, and not one of them is the reason no seat can launch on that box. The host installs a fleet, starts a fleet, and runs nothing. mosaic fleet ps is the first and only signal, and by then the operator is debugging systemd.

The fix

A check_fleet_transport in each of the two places, reading the roster's own transport: field rather than assuming tmux.

The installer warns; it does not hard-fail. tmux is required by the fleet, not by mosaic. Plenty of hosts install this to run mosaic claude and will never scaffold a roster; failing their install over a binary they do not need would be wrong. The warning names exactly what it blocks and says the rest is unaffected. It runs in --check mode as well as normal mode — "what is the state of this host" is precisely what --check is asked.

Why two implementations, and how they are kept honest

The installer has to be able to answer this before the framework's own scripts are guaranteed to be on disk, so it cannot call the doctor's copy. Two implementations of one rule is exactly the shape that drifts.

packages/mosaic/framework/tools/_scripts/test-fleet-transport-check.sh drives both, in one file, from one table of cases — and it extracts the functions from the shipped scripts by awk rather than carrying a copy. A test with its own copy of the logic is a test that keeps passing after the shipped copy changes, which is the failure mode this whole area keeps producing. If either script is reshaped so the extraction stops matching, the harness fails loudly instead of silently measuring an empty string.

The absent-transport cases run on a PATH containing only sed head tr awk, and the harness refuses to run at all if tmux turns out to be resolvable there — a green that measured nothing is the thing being guarded against.

Wiring, and the guard that caught me

The harness is registered in test:framework-shell. It is one line of package.json, and it matters: with the file on disk and unwired, check-test-enumeration.sh (#1017) fails it as UNENUMERATED. That guard is correct and it did its job here. A check nothing runs is not a check.

Evidence

Red — against origin/next:

FAIL: could not extract 'fleet_declared_transport' from .../mosaic-doctor
rc=1

plus grep -ci tmux = 0 on both shipped files.

Green, on real hosts, all four branches:

host state result
dev no tmux, no roster [WARN] Fleet transport 'tmux' is not installed — 'mosaic fleet' cannot run seats here. Install it (e.g. sudo apt-get install -y tmux) before 'mosaic fleet init'.
dev no tmux, v2 roster [WARN] … this host has a roster and no seat can launch. Install it …, then 'mosaic fleet start'.
dev install.sh --check ⚠ Fleet transport 'tmux' is not installed. + "mosaic fleet start reports success and no seat comes up"
canary tmux present, v2 roster [OK] Fleet transport available: tmux under --verbose; silent by default (pass() is verbose-gated); installer silent

Both boxes were restored to their prior scripts afterward.

Harness runs green on node:24-alpine (busybox sed/awk), the CI base image — checked before adding it to CI rather than after.

Gates: bash -n ×3 · pnpm typecheck 45/45 · fleet specs 342 passed · enumeration guard OK (population 52, enumerated 33) · its self-test OK · prettier --check clean on package.json.

Where this sits in the chain

Upstream of both open fixes. A correct fix for #1237/#1243 or #1241/#1244 still leaves a greenfield host with install rc=0, start rc=0, and no live seat — because tmux is not there. This is the link that says so out loud.

Still downstream and unowned after this: no agent runtime (claude/pi/codex/opencode) is installed by anything, which #1244's launcher check will now report as code=missing-binary instead of a silent success.

Notes for the reviewer

  • Two places worth arguing with me about: warn-vs-fail in the installer, and running the check in --check mode. Both are deliberate and both are in the comments.
  • pass() in mosaic-doctor only prints under --verbose; the harness stubs it unconditionally on purpose, because what is under test is which severity the check chose, not whether default verbosity shows it. That is stated in the harness.
Closes #1240. ## The bug in one line `grep -ci tmux` on `tools/install.sh` and on `tools/_scripts/mosaic-doctor` at `origin/next` returns **0** for both. The fleet runs entirely inside tmux, the installer never installs it, and the audit script never mentions it. ## What that costs an operator Measured on `mosaic-sbx-dev` — Debian, greenfield, framework installed, no tmux: ``` [mosaic-doctor] warnings=11 ``` Eleven warnings, and not one of them is the reason no seat can launch on that box. The host installs a fleet, starts a fleet, and runs nothing. `mosaic fleet ps` is the first and only signal, and by then the operator is debugging systemd. ## The fix A `check_fleet_transport` in each of the two places, reading the roster's own `transport:` field rather than assuming tmux. **The installer warns; it does not hard-fail.** tmux is required by the *fleet*, not by *mosaic*. Plenty of hosts install this to run `mosaic claude` and will never scaffold a roster; failing their install over a binary they do not need would be wrong. The warning names exactly what it blocks and says the rest is unaffected. It runs in `--check` mode as well as normal mode — "what is the state of this host" is precisely what `--check` is asked. ## Why two implementations, and how they are kept honest The installer has to be able to answer this **before the framework's own scripts are guaranteed to be on disk**, so it cannot call the doctor's copy. Two implementations of one rule is exactly the shape that drifts. `packages/mosaic/framework/tools/_scripts/test-fleet-transport-check.sh` drives **both**, in one file, from one table of cases — and it **extracts the functions from the shipped scripts by awk rather than carrying a copy**. A test with its own copy of the logic is a test that keeps passing after the shipped copy changes, which is the failure mode this whole area keeps producing. If either script is reshaped so the extraction stops matching, the harness fails loudly instead of silently measuring an empty string. The absent-transport cases run on a PATH containing only `sed head tr awk`, and the harness refuses to run at all if tmux turns out to be resolvable there — a green that measured nothing is the thing being guarded against. ## Wiring, and the guard that caught me The harness is registered in `test:framework-shell`. It is one line of `package.json`, and it matters: with the file on disk and unwired, `check-test-enumeration.sh` (#1017) fails it as `UNENUMERATED`. That guard is correct and it did its job here. A check nothing runs is not a check. ## Evidence **Red** — against `origin/next`: ``` FAIL: could not extract 'fleet_declared_transport' from .../mosaic-doctor rc=1 ``` plus `grep -ci tmux` = 0 on both shipped files. **Green, on real hosts, all four branches:** | host | state | result | |---|---|---| | dev | no tmux, no roster | `[WARN] Fleet transport 'tmux' is not installed — 'mosaic fleet' cannot run seats here. Install it (e.g. sudo apt-get install -y tmux) before 'mosaic fleet init'.` | | dev | no tmux, v2 roster | `[WARN] … this host has a roster and no seat can launch. Install it …, then 'mosaic fleet start'.` | | dev | `install.sh --check` | `⚠ Fleet transport 'tmux' is not installed.` + "mosaic fleet start reports success and no seat comes up" | | canary | tmux present, v2 roster | `[OK] Fleet transport available: tmux` under `--verbose`; silent by default (`pass()` is verbose-gated); installer silent | Both boxes were restored to their prior scripts afterward. **Harness runs green on `node:24-alpine`** (busybox `sed`/`awk`), the CI base image — checked before adding it to CI rather than after. **Gates:** `bash -n` ×3 · `pnpm typecheck` 45/45 · fleet specs 342 passed · enumeration guard OK (population 52, enumerated 33) · its self-test OK · `prettier --check` clean on `package.json`. ## Where this sits in the chain Upstream of both open fixes. **A correct fix for #1237/#1243 or #1241/#1244 still leaves a greenfield host with `install` rc=0, `start` rc=0, and no live seat** — because tmux is not there. This is the link that says so out loud. Still downstream and unowned after this: no agent runtime (`claude`/`pi`/`codex`/`opencode`) is installed by anything, which #1244's launcher check will now report as `code=missing-binary` instead of a silent success. ## Notes for the reviewer - Two places worth arguing with me about: warn-vs-fail in the installer, and running the check in `--check` mode. Both are deliberate and both are in the comments. - `pass()` in `mosaic-doctor` only prints under `--verbose`; the harness stubs it unconditionally on purpose, because what is under test is which severity the check *chose*, not whether default verbosity shows it. That is stated in the harness.
fred added 1 commit 2026-08-16 05:16:40 +00:00
`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.
Author
Collaborator

Confirmed working on a genuinely greenfield box. The installer emitted the transport warning on a VM with no tmux — naming the package, stating that fleet start would otherwise report success with no seat up, and giving the apt command. With tmux installed the warning is silent and --verbose shows [OK] Fleet transport available: tmux.

Independently corroborated downstream: fleet start on that same box failed with refusing unmanaged Mosaic tmux server on socket mosaic-fleet: tmux binary is unavailable (exit 64) — exactly the failure the warning predicts. Full table in the comment on #1244.

This and #1244 should land together.

Confirmed working on a genuinely greenfield box. The installer emitted the transport warning on a VM with no tmux — naming the package, stating that `fleet start` would otherwise report success with no seat up, and giving the apt command. With tmux installed the warning is silent and `--verbose` shows `[OK] Fleet transport available: tmux`. Independently corroborated downstream: `fleet start` on that same box failed with `refusing unmanaged Mosaic tmux server on socket mosaic-fleet: tmux binary is unavailable` (exit 64) — exactly the failure the warning predicts. Full table in the comment on #1244. This and #1244 should land together.
Author
Collaborator

⚠️ Pre-merge gate: both install.sh files are a hand-resolved keep-both

Raised by @scooby reviewing the E2E, and it is the right catch. Recording it here so it cannot be
lost between now and the merge.

The risk. #1245 and the installer PRs (#1229/#1242) both append a function after
require_cmd() in tools/install.sh, and the shared trailing } closes whichever side wins.
Git raises this conflict again when the PRs are merged one at a time. My greenfield E2E validated
my resolution. If whoever merges resolves it differently, the install.sh that ships is not
the install.sh the E2E measured, and both installer rows of that table become unproven.

Same family as #1249the thing that runs is not the thing that shipped — except here it
happens at the merge, not in the code.

The resolution that was measured. Keep both functions, each with its own closing brace, in
this order:

  • tools/install.shensure_prefix_on_path() at :380, closing :391; then
    check_fleet_transport() at :410, closing :428. Call sites: ensure_prefix_on_path at
    :970/:983/:1001, check_fleet_transport at :1149.
  • packages/mosaic/framework/install.sh#1242's mode-setting (+58: umask 022 and chmod 700
    on $TARGET_DIR, fleet, fleet/agents, credentials, each warn-on-failure) plus #1245's
    transport-check append.

Neither side is a rewrite of the other; the conflict is purely that two additions share a brace.

The check — one command, exact values. After the five land on next:

git fetch origin
git rev-parse origin/next:tools/install.sh origin/next:packages/mosaic/framework/install.sh

Must print exactly:

5d28f773c63e8e71f55f65f9c41221c5e16571e6      # tools/install.sh
1578c33bc0207203d2093d5120f1cafec8610292      # packages/mosaic/framework/install.sh

Equivalently, and easier to eyeball:

git diff origin/next origin/e2e-compose -- '*install.sh'     # must be empty

If those match, the entire E2E table transfers to next unchanged. If they do not, the two
installer rows are unproven and I will re-run the greenfield before anyone relies on them.

e2e-compose is pushed and stays put as the reference tree until this is confirmed.

## ⚠️ Pre-merge gate: both `install.sh` files are a hand-resolved keep-both Raised by @scooby reviewing the E2E, and it is the right catch. Recording it here so it cannot be lost between now and the merge. **The risk.** #1245 and the installer PRs (#1229/#1242) both append a function after `require_cmd()` in `tools/install.sh`, and the shared trailing `}` closes whichever side wins. Git raises this conflict again when the PRs are merged one at a time. My greenfield E2E validated **my** resolution. If whoever merges resolves it differently, the `install.sh` that ships is not the `install.sh` the E2E measured, and both installer rows of that table become unproven. Same family as #1249 — *the thing that runs is not the thing that shipped* — except here it happens at the merge, not in the code. **The resolution that was measured.** Keep both functions, each with its own closing brace, in this order: - `tools/install.sh` — `ensure_prefix_on_path()` at :380, closing :391; then `check_fleet_transport()` at :410, closing :428. Call sites: `ensure_prefix_on_path` at :970/:983/:1001, `check_fleet_transport` at :1149. - `packages/mosaic/framework/install.sh` — #1242's mode-setting (+58: `umask 022` and `chmod 700` on `$TARGET_DIR`, `fleet`, `fleet/agents`, `credentials`, each warn-on-failure) plus #1245's transport-check append. Neither side is a rewrite of the other; the conflict is purely that two additions share a brace. **The check — one command, exact values.** After the five land on `next`: ``` git fetch origin git rev-parse origin/next:tools/install.sh origin/next:packages/mosaic/framework/install.sh ``` Must print exactly: ``` 5d28f773c63e8e71f55f65f9c41221c5e16571e6 # tools/install.sh 1578c33bc0207203d2093d5120f1cafec8610292 # packages/mosaic/framework/install.sh ``` Equivalently, and easier to eyeball: ``` git diff origin/next origin/e2e-compose -- '*install.sh' # must be empty ``` **If those match, the entire E2E table transfers to `next` unchanged.** If they do not, the two installer rows are unproven and I will re-run the greenfield before anyone relies on them. `e2e-compose` is pushed and stays put as the reference tree until this is confirmed.
fred added 1 commit 2026-08-16 18:11:33 +00:00
merge next into fix/1240-fleet-transport-check
ci/woodpecker/pr/ci Pipeline failed
5198c3f198
Resolves conflicts from #1229 (tools/install.sh node provisioning) and #1252
(package.json test:framework-shell). tools/install.sh resolved to the reviewed
composite blob 5d28f773; package.json resolved as a union so both #1252's four
suites and #1245's transport-check suite run (48 links).
fred merged commit 476db12b92 into next 2026-08-16 18:12:34 +00:00
Sign in to join this conversation.