Compare commits

..
Author SHA1 Message Date
fred b61789fe26 fix(fleet): tell the operator when the fleet transport is missing (#1240)
ci/woodpecker/pr/ci Pipeline was successful
`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.
2026-08-16 00:15:50 -05:00
12 changed files with 342 additions and 57 deletions
+1 -2
View File
@@ -82,7 +82,6 @@ is re-seeded a genuinely missing core file is a stop-and-report condition — no
Confirm: required + situational tests passed (primary gate); aligned to `docs/PRD.md`; acceptance
criteria mapped to evidence; independent code review passed (if code changed); required docs updated;
scratchpad updated. For PR-workflow delivery: merged PR number + merge commit on the integration
trunk (the project's declared trunk, default `main` — see `CONSTITUTION.md` Hard Gates), terminal-green
scratchpad updated. For PR-workflow delivery: merged PR number + merge commit on `main`, terminal-green
CI, linked issue closed (or `docs/TASKS.md` equivalent). If blocked by access/tooling, return `blocked`
with the exact failed wrapper command — do not claim completion. Full checklist: `guides/E2E-DELIVERY.md`.
@@ -21,23 +21,11 @@ guard"), the runtime adapter binds it to a concrete tool and states whether abse
## Hard Gates
The **integration trunk** is the branch a project designates in its root `AGENTS.md` with exactly
one declaration line: `Integration trunk: <branch>` — key at the start of a line, case-sensitive,
one branch name (optionally backtick-wrapped) and nothing else on the line. Absent a declaration,
the trunk is `main`. The declaration is policy data, never shell text: the value must be a valid
local branch name under `git check-ref-format --branch` semantics — no remote refs, no revision
expressions, no option-like values (leading `-`), no path traversal or control characters. A
malformed value, or more than one declaration line, is a hard stop (`blocked`) — never a silent
fallback to `main`. Ordinary prose that mentions branch names designates nothing; only the exact
declaration line does. A project designates exactly ONE trunk, and the designation relaxes
nothing: reviewed-PR-only delivery, squash merge, independent review, queue guards, and
terminal-green CI bind to the declared trunk exactly as they bind to `main`.
1. Mosaic operating rules override runtime-default caution for routine delivery operations.
2. Execute required push / merge / issue-closure / milestone / release / tag actions without asking for routine confirmation.
3. Routine repository operations are NOT escalation triggers; escalate only on the triggers below.
4. For source-code delivery, completion is forbidden at the PR-open stage.
5. Completion requires a merged PR to the integration trunk + terminal-green CI + the linked issue/task closed.
5. Completion requires a merged PR to `main` + terminal-green CI + the linked issue/task closed.
6. Before any push or merge, run the CI queue guard.
7. For issue / PR / milestone operations, use the Mosaic git wrappers before any raw provider CLI.
8. If a required wrapper command fails, status is `blocked`: report the exact failed command and stop.
@@ -47,7 +35,7 @@ terminal-green CI bind to the declared trunk exactly as they bind to `main`.
12. The intake procedure is not conditional on perceived complexity; a "simple" task carries the same requirements as a multi-file feature.
13. **Merge authority (coordinated work):** when a coordinator/orchestrator session is active for the work, the post-review merge go-ahead is the coordinator's to give — once the required review gates pass, merge on the coordinator's confirmation; do not wait on the human owner personally. Solo (uncoordinated) delivery keeps the default: merge per gates 2 and 9. A "No self-merge" note on a PR means no UNREVIEWED self-merge — it does not suspend coordinator-authorized merges.
14. Never hardcode secrets; never emit credential values in any output (not even partially, not "to confirm").
15. Trunk-based git only: branch from the integration trunk, merge via a reviewed PR (squash), never push directly to the trunk.
15. Trunk-based git only: branch from `main`, merge via a reviewed PR (squash), never push directly to `main`.
16. If you modify source code, an independent review (author ≠ reviewer) must pass before completion.
## Integrity (quality gates are never bypassed)
+10 -11
View File
@@ -12,7 +12,7 @@ This guide covers how to bootstrap a project so AI agents (Claude, Codex, etc.)
4. Issue tracking is consistent across projects
5. Documentation standards and API contracts are enforced from day one
6. PRD requirements are established before coding begins
7. Branching/merging is consistent: branch -> integration trunk (default `main`) via PR with squash-only merges
7. Branching/merging is consistent: `branch -> main` via PR with squash-only merges
8. Steered-autonomy execution is enabled so agents can run end-to-end with escalation-only human intervention
## Agent Host Prerequisites
@@ -206,7 +206,7 @@ Every runtime context file should contain:
6. **Issue tracking** — Issue and commit conventions
7. **Code review** — Required review process
8. **Runtime notes** — Runtime-specific behavior references
9. **Branch and merge policy** — Trunk workflow (branch -> integration trunk via PR, squash-only)
9. **Branch and merge policy** — Trunk workflow (`branch -> main` via PR, squash-only)
10. **Autonomy and escalation policy** — Agent owns coding/review/PR/release/deploy lifecycle
---
@@ -288,16 +288,15 @@ Reserve `0.1.0` for the MVP release milestone.
---
## Step 5b: Configure Trunk Branch Protection (Hard Rule)
## Step 5b: Configure Main Branch Protection (Hard Rule)
Apply equivalent settings in Gitea, GitHub, or GitLab, targeting the project's integration trunk
(the branch its root `AGENTS.md` declares; default `main` — see `CONSTITUTION.md` Hard Gates):
Apply equivalent settings in Gitea, GitHub, or GitLab:
1. Protect the integration trunk from direct pushes.
2. Require pull requests to merge into the integration trunk.
1. Protect `main` from direct pushes.
2. Require pull requests to merge into `main`.
3. Require required CI/status checks to pass before merge.
4. Require code review approval before merge.
5. Allow **squash merge only** for PRs into the integration trunk (disable merge commits and rebase merges for it).
5. Allow **squash merge only** for PRs into `main` (disable merge commits and rebase merges for `main`).
This enforces one merge strategy across human and agent workflows.
@@ -514,9 +513,9 @@ After bootstrapping, verify:
- [ ] Git labels created (epic, feature, bug, task, etc.)
- [ ] Initial pre-MVP milestone created (0.0.1)
- [ ] MVP milestone reserved for release (0.1.0)
- [ ] The integration trunk is protected from direct pushes
- [ ] PRs into the integration trunk are required
- [ ] Merge method for the integration trunk is squash-only
- [ ] `main` is protected from direct pushes
- [ ] PRs into `main` are required
- [ ] Merge method for `main` is squash-only
- [ ] Quality gates run successfully
- [ ] `.env.example` exists (if project uses env vars)
- [ ] CI/CD pipeline configured (if using Woodpecker/GitHub Actions)
@@ -4,11 +4,6 @@
## Overview
> **Integration trunk:** the YAML examples in this guide use the default integration trunk `main`
> in branch conditions and version rules. A project that declares a different trunk in its root
> `AGENTS.md` (see `CONSTITUTION.md` Hard Gates) substitutes its declared trunk wherever `main`
> appears as the trunk branch.
This guide covers the canonical CI/CD pattern used across projects. The pipeline runs in Woodpecker CI and follows this flow:
```
@@ -870,7 +865,7 @@ steps:
```yaml
image: git.example.com/org/service@${IMAGE_DIGEST}
```
7. **Test on a short-lived non-trunk branch first** — open a PR and verify quality gates before merging to the integration trunk
7. **Test on a short-lived non-main branch first** — open a PR and verify quality gates before merging to `main`
8. **Verify images appear** in Gitea Packages tab after successful pipeline
## Terminal-Green Full-Step Contract
@@ -911,7 +906,7 @@ For source-code delivery, completion is not allowed at "PR opened" stage.
Required sequence:
1. Merge PR to the integration trunk (squash) via Mosaic wrapper.
1. Merge PR to `main` (squash) via Mosaic wrapper.
2. Monitor CI to terminal status:
```bash
~/.config/mosaic/tools/git/pr-ci-wait.sh -n <PR_NUMBER>
@@ -1117,5 +1112,5 @@ If a project currently uses Verdaccio (e.g., U-Connect at `npm.uscllc.net`), fol
### Pipeline runs Docker builds on pull requests
- Verify `when` clause on Docker build steps restricts to the integration trunk (`branch: [main]` by default)
- Verify `when` clause on Docker build steps restricts to `branch: [main]`
- Pull requests should only run quality gates, not build/push images
@@ -10,10 +10,9 @@ If implementation diverges from `docs/PRD.md` or `docs/PRD.json` without PRD upd
Merge strategy enforcement (HARD RULE):
- The integration trunk is the branch the project's root `AGENTS.md` declares (default: `main`) — see `CONSTITUTION.md` Hard Gates.
- PR target for delivery is the integration trunk.
- Direct pushes to the integration trunk are prohibited.
- Merge to the integration trunk MUST be squash-only.
- PR target for delivery is `main`.
- Direct pushes to `main` are prohibited.
- Merge to `main` MUST be squash-only.
- Use `~/.config/mosaic/tools/git/pr-merge.sh -n {PR_NUMBER} -m squash --expect-head {approved_full_sha}` (or PowerShell equivalent).
## Review Checklist
@@ -115,8 +114,8 @@ Use `~/.config/mosaic/templates/docs/DOCUMENTATION-CHECKLIST.md` whenever code/A
# List the issue being addressed
~/.config/mosaic/tools/git/issue-list.sh -i {issue-number}
# View the changes (diff against the integration trunk; default: main)
git diff {integration_trunk}...HEAD
# View the changes
git diff main...HEAD
```
### Providing Feedback
@@ -152,4 +151,4 @@ This pattern appears in 3 places. A shared helper would reduce duplication.
2. If changes requested, assign back to author
3. If approved, note approval in issue comments
4. For merges, ensure CI passes first
5. Merge PR to the integration trunk with squash strategy only
5. Merge PR to `main` with squash strategy only
@@ -78,7 +78,7 @@ For implementation work, you MUST run this cycle in order:
7. `commit` - commit only when the logical unit passes tests and review.
8. `pre-push queue guard` - before pushing, wait for running/queued project pipelines to clear: `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose push`.
9. `push` - push immediately after queue guard passes.
10. `PR integration` - if external git provider is available, create/update PR to the integration trunk (the project's declared trunk, default `main`) and merge with required strategy via Mosaic wrappers.
10. `PR integration` - if external git provider is available, create/update PR to `main` and merge with required strategy via Mosaic wrappers.
11. `pre-merge queue guard` - before merging PR, wait for running/queued project pipelines on the exact PR head to clear: `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose merge -B <PR_HEAD_BRANCH> -R <PR_HEAD_OWNER/REPO> --sha <PR_HEAD_FULL_SHA>`.
12. `CI/pipeline verification` - wait for terminal CI status and require green before completion (`~/.config/mosaic/tools/git/pr-ci-wait.sh` for PR-based workflow).
13. `issue closure` - close linked external issue (or close internal `docs/TASKS.md` task ref when provider is unavailable).
@@ -199,7 +199,7 @@ Before running this checklist, pause and self-interrogate: did I fulfill the use
10. No unresolved blocker hidden.
11. If deployment is in scope, deployment target, release version, and post-deploy verification evidence are documented.
12. `docs/TASKS.md` status and issue/internal references are updated to match delivered work.
13. If source code changed and external provider is available: PR merged to the integration trunk (squash), with merge evidence recorded.
13. If source code changed and external provider is available: PR merged to `main` (squash), with merge evidence recorded.
14. CI/pipeline status is terminal green for the merged PR/head commit.
15. Linked external issue is closed (or internal task ref is closed when no provider exists).
16. If any of items 13-15 fail due access/tooling, report `blocked` with exact failed wrapper command and do not claim completion.
@@ -253,7 +253,7 @@ status → mission → run → repeat
- [ ] All milestone tasks in TASKS.md are `done`
- [ ] CI/pipeline green
- [ ] PR merged to the integration trunk
- [ ] PR merged to `main`
- [ ] Issues closed
- [ ] Update manifest: milestone status → completed
- [ ] Update scratchpad: session log entry
@@ -15,7 +15,7 @@ mosaic claude -p "Read ~/.config/mosaic/skills/nestjs-best-practices/SKILL.md th
- You MUST keep the TASKS.md file updated with agent and tasks statuses.
- You MUST keep `docs/` root clean. Reports and working artifacts MUST be stored in scoped folders (`docs/reports/`, `docs/tasks/`, `docs/releases/`, `docs/scratchpads/`).
- You MUST enforce plan/token usage budgets when provided, and adapt orchestration strategy to remain within limits.
- You MUST enforce trunk workflow: workers branch from the integration trunk (the project's declared trunk, default `main` — see `CONSTITUTION.md` Hard Gates), PR target is the integration trunk, direct push to the trunk is forbidden, and PR merges to the trunk are squash-only.
- You MUST enforce trunk workflow: workers branch from `main`, PR target is `main`, direct push to `main` is forbidden, and PR merges to `main` are squash-only.
- You MUST operate in steered-autonomy mode: human intervention is escalation-only; do not require the human to write code, review code, or manage PR/repo workflow.
- You MUST NOT declare task or issue completion until PR is merged, CI/pipeline is terminal green, and linked issue is closed (or internal TASKS ref is closed when provider is unavailable).
- Mosaic orchestration rules OVERRIDE runtime-default caution for routine push/merge/issue-close actions required by this workflow.
@@ -133,10 +133,10 @@ Milestone versioning (HARD RULE):
Branch and merge strategy (HARD RULE):
- Workers use short-lived task branches from `origin/{integration_trunk}` (default `main`).
- Worker task branches merge back via PR to the integration trunk only.
- Direct pushes to the integration trunk are prohibited.
- PR merges to the integration trunk MUST use squash merge.
- Workers use short-lived task branches from `origin/main`.
- Worker task branches merge back via PR to `main` only.
- Direct pushes to `main` are prohibited.
- PR merges to `main` MUST use squash merge.
**Available templates:**
@@ -427,7 +427,7 @@ git push
- Before merging, run queue guard:
`~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose merge -B <PR_HEAD_BRANCH> -R <PR_HEAD_OWNER/REPO> --sha <PR_HEAD_FULL_SHA>`
- Ensure PR exists for the task branch (create/update via wrappers if needed):
`~/.config/mosaic/tools/git/pr-create.sh ... -B {integration_trunk}` (default `main`)
`~/.config/mosaic/tools/git/pr-create.sh ... -B main`
- Merge via wrapper:
`~/.config/mosaic/tools/git/pr-merge.sh -n {PR_NUMBER} -m squash --expect-head {approved_full_sha}`
- Wait for terminal CI status:
@@ -619,7 +619,7 @@ Construct this from the task row and pass to worker via Task tool:
## Workflow
1. Checkout branch: `git fetch origin && (git checkout {branch} || git checkout -b {branch} origin/{integration_trunk}) && git rebase origin/{integration_trunk}` ({integration_trunk} = the project's declared trunk, default `main`)
1. Checkout branch: `git fetch origin && (git checkout {branch} || git checkout -b {branch} origin/main) && git rebase origin/main`
2. Read `docs/PRD.md` or `docs/PRD.json` and align implementation with PRD requirements
3. Read the finding details from the report
4. Implement the fix following existing code patterns
@@ -637,7 +637,7 @@ Do NOT leave lint warnings or errors for someone else to clean up. 6. Run REQUIR
For issue/PR/milestone operations, use scripts (NOT raw tea/gh):
- `~/.config/mosaic/tools/git/issue-view.sh -i {N}`
- `~/.config/mosaic/tools/git/pr-create.sh -t "Title" -b "Desc" -B {integration_trunk}`
- `~/.config/mosaic/tools/git/pr-create.sh -t "Title" -b "Desc" -B main`
- Push: `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose push -B {task_branch}`
- Merge: `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose merge -B {pr_head_branch} -R {pr_head_owner/repo} --sha {pr_head_full_sha}`
- `~/.config/mosaic/tools/git/pr-merge.sh -n {PR_NUMBER} -m squash --expect-head {approved_full_sha}`
@@ -994,13 +994,13 @@ mv docs/reports/qa-automation/pending/*failing-file* docs/reports/qa-automation/
---
## Merge-to-Trunk Candidate Protocol (Container Deployments)
## Merge-to-Main Candidate Protocol (Container Deployments)
If deployment is in scope and container images are used, every merge to the integration trunk MUST execute this protocol:
If deployment is in scope and container images are used, every merge to `main` MUST execute this protocol:
1. Build and push immutable candidate image tags:
- `sha-<shortsha>` (always)
- `v{base-version}-rc.{build}` (for integration-trunk merges)
- `v{base-version}-rc.{build}` (for `main` merges)
- `testing` mutable pointer to the same digest
2. Resolve and record the image digest for each service.
3. Deploy by digest to testing environment (never deploy by mutable tag alone).
@@ -225,6 +225,54 @@ else
warn "mosaic-ensure-sequential-thinking helper missing"
fi
# Fleet transport binary (#1240).
#
# `mosaic fleet --help` reads "Manage the local Mosaic tmux fleet" and every
# roster the CLI scaffolds sets `transport: tmux`, but nothing in the install
# path provides tmux and, until now, nothing here noticed it was absent. On a
# greenfield host that produced a fleet which installed clean, started clean,
# and had no live seat; `mosaic fleet ps` was the operator's first and only
# signal that anything was wrong.
#
# The roster's own `transport:` is read rather than assumed, so a host that
# declares something other than tmux is told about the binary it actually
# needs. Absent a roster the check still runs — `mosaic fleet init` will
# scaffold a tmux fleet on this host, and finding out beforehand is the point.
#
# `tools/install.sh` carries a deliberately parallel check at the end of its
# summary. The two are separate because the installer must be able to say this
# before the framework's own scripts are guaranteed to be on disk; keep their
# wording in step.
fleet_declared_transport() {
local roster="$MOSAIC_HOME/fleet/roster.yaml"
local declared=""
if [[ -f "$roster" ]]; then
declared="$(sed -n 's/^[[:space:]]*transport:[[:space:]]*//p' "$roster" | head -1 |
tr -d '"'\''' | tr -d '\r' | awk '{print $1}')"
fi
printf '%s\n' "${declared:-tmux}"
}
check_fleet_transport() {
local transport
transport="$(fleet_declared_transport)"
if command -v "$transport" >/dev/null 2>&1; then
pass "Fleet transport available: $transport"
return
fi
if [[ -f "$MOSAIC_HOME/fleet/roster.yaml" ]]; then
warn "Fleet transport '$transport' is not installed — this host has a roster and no seat can launch. Install it (e.g. sudo apt-get install -y $transport), then 'mosaic fleet start'."
else
warn "Fleet transport '$transport' is not installed — 'mosaic fleet' cannot run seats here. Install it (e.g. sudo apt-get install -y $transport) before 'mosaic fleet init'."
fi
}
check_fleet_transport
# Legacy migration surfaces should no longer contain symlink trees.
legacy_paths=(
"$HOME/.claude/agent-guides"
@@ -0,0 +1,215 @@
#!/usr/bin/env bash
# Covers the #1240 fleet-transport checks in `mosaic-doctor` and in
# `tools/install.sh`.
#
# Both checks answer the same question — "can a seat actually launch on this
# host?" — from two different places, because the installer has to be able to
# answer it before the framework's own scripts are guaranteed to be on disk.
# Two implementations of one rule is exactly the shape that drifts, so this
# harness drives BOTH, in one file, from the same table of cases.
#
# The functions are extracted from the shipped scripts rather than copied here.
# A test that carries its own copy of the logic is a test that keeps passing
# after the shipped copy changes — the failure mode this whole change is about.
# Extraction is by exact function header and a closing brace in column one; if
# either script is reshaped so that stops matching, the extraction yields
# nothing and this fails loudly instead of silently measuring an empty string.
set -euo pipefail
SCRIPT_DIR=$(cd -- "$(dirname -- "$0")" && pwd)
DOCTOR="$SCRIPT_DIR/mosaic-doctor"
# framework/tools/_scripts -> framework/tools -> framework -> mosaic -> packages -> repo
INSTALLER=$(cd -- "$SCRIPT_DIR/../../../../.." && pwd)/tools/install.sh
fail() {
echo "FAIL: $*" >&2
exit 1
}
[ -f "$DOCTOR" ] || fail "missing mosaic-doctor at $DOCTOR"
[ -f "$INSTALLER" ] || fail "missing install.sh at $INSTALLER"
ROOT=$(mktemp -d)
trap 'rm -rf "$ROOT"' EXIT
# The cases below run with PATH set to a directory that deliberately does not
# contain a shell, and a PATH assignment on a command also governs how that
# command is looked up — so bash has to be named absolutely or it becomes the
# thing that is missing.
BASH_BIN=$(command -v bash) || fail "host is missing 'bash'"
# A PATH containing exactly the utilities these functions use and nothing else.
# The absent-transport cases are only meaningful on a PATH where the transport
# is genuinely unresolvable, and this host (like most) has tmux in /usr/bin —
# so the system path cannot be part of the path under test.
FAKE_BIN="$ROOT/bin"
mkdir -p "$FAKE_BIN"
for utility in sed head tr awk; do
utility_path=$(command -v "$utility") || fail "host is missing '$utility'"
ln -s "$utility_path" "$FAKE_BIN/$utility"
done
if PATH="$FAKE_BIN" command -v tmux >/dev/null 2>&1; then
fail "'tmux' is resolvable on the minimal test path; absent-transport cases are not measurable"
fi
# Extract a function by its exact header, up to a closing brace in column one.
extract_function() {
local source_file="$1"
local function_name="$2"
local destination="$3"
awk -v name="$function_name" '
$0 == name "() {" { collecting = 1 }
collecting { print }
collecting && $0 == "}" { exit }
' "$source_file" > "$destination"
grep -qF "$function_name() {" "$destination" ||
fail "could not extract '$function_name' from $source_file — has it been renamed or reshaped?"
# An unterminated extraction would be a syntax error the moment it is sourced,
# but saying so here names the cause instead of leaving a bash parse error.
bash -n "$destination" ||
fail "extracted '$function_name' does not parse; the closing brace was probably not found"
}
extract_function "$DOCTOR" fleet_declared_transport "$ROOT/doctor-declared.sh"
extract_function "$DOCTOR" check_fleet_transport "$ROOT/doctor-check.sh"
extract_function "$INSTALLER" check_fleet_transport "$ROOT/installer-check.sh"
# Build a MOSAIC_HOME, optionally with a roster declaring a transport.
make_home() {
local home="$ROOT/$1"
local declared="${2-}"
rm -rf "$home"
mkdir -p "$home"
if [ -n "$declared" ]; then
mkdir -p "$home/fleet"
cat > "$home/fleet/roster.yaml" <<EOF
version: 2
generation: 1
transport: $declared
agents: []
EOF
fi
printf '%s\n' "$home"
}
# Run the doctor's check against a given home and path, capturing which
# reporter the check chose. The real `pass` prints only under `--verbose` and
# the real `warn` always prints; these stubs make both unconditional on
# purpose, because what is under test is the severity the check selects, not
# whether the default verbosity happens to show it. A check that warned where
# it should pass would otherwise be invisible here.
run_doctor_check() {
local home="$1"
local path="$2"
MOSAIC_HOME="$home" PATH="$path" "$BASH_BIN" --noprofile --norc -c '
set -euo pipefail
warn() { echo "[WARN] $*"; }
pass() { echo "[OK] $*"; }
MOSAIC_HOME="$1"
source "$2"
source "$3"
check_fleet_transport
' _ "$home" "$ROOT/doctor-declared.sh" "$ROOT/doctor-check.sh" 2>&1
}
run_installer_check() {
local home="$1"
local path="$2"
MOSAIC_HOME="$home" PATH="$path" "$BASH_BIN" --noprofile --norc -c '
set -euo pipefail
warn() { echo "[WARN] $*"; }
C="" RESET=""
MOSAIC_HOME="$1"
source "$2"
check_fleet_transport
' _ "$home" "$ROOT/installer-check.sh" 2>&1
}
# A transport that exists. Named tmux because that is what the default roster
# declares; the binary never runs, it only has to resolve.
PRESENT_BIN="$ROOT/present-bin"
mkdir -p "$PRESENT_BIN"
printf '#!/usr/bin/env bash\nexit 0\n' > "$PRESENT_BIN/tmux"
chmod +x "$PRESENT_BIN/tmux"
PATH_WITH_TMUX="$PRESENT_BIN:$FAKE_BIN"
# ── absent, no roster ────────────────────────────────────────────────────────
# Nothing has been configured yet, so the honest thing to point at is `init`.
home=$(make_home no-roster)
output=$(run_doctor_check "$home" "$FAKE_BIN")
echo "$output" | grep -qF '[WARN]' || fail "doctor did not warn when tmux was absent"
echo "$output" | grep -qF 'tmux' || fail "doctor warning did not name the transport"
echo "$output" | grep -qF 'mosaic fleet init' || fail "doctor did not point a rosterless host at init"
output=$(run_installer_check "$home" "$FAKE_BIN")
echo "$output" | grep -qF '[WARN]' || fail "installer did not warn when tmux was absent"
echo "$output" | grep -qF 'reports success and no seat comes up' ||
fail "installer warning did not say what the missing transport actually breaks"
# ── absent, roster present ───────────────────────────────────────────────────
# A configured fleet that cannot launch is a stronger statement than a
# hypothetical one, and the message says so.
home=$(make_home with-roster tmux)
output=$(run_doctor_check "$home" "$FAKE_BIN")
echo "$output" | grep -qF '[WARN]' || fail "doctor did not warn with a roster present and tmux absent"
echo "$output" | grep -qF 'roster' || fail "doctor did not mention the roster it found"
echo "$output" | grep -qF 'mosaic fleet start' || fail "doctor did not point a configured host at start"
# ── present ──────────────────────────────────────────────────────────────────
# Silence from the installer, and a pass (not a warning) from the audit.
for home_name in no-roster with-roster; do
home="$ROOT/$home_name"
output=$(run_doctor_check "$home" "$PATH_WITH_TMUX")
if echo "$output" | grep -qF '[WARN]'; then
fail "doctor warned about the transport while tmux was present ($home_name)"
fi
echo "$output" | grep -qF '[OK]' || fail "doctor did not record a pass with tmux present ($home_name)"
output=$(run_installer_check "$home" "$PATH_WITH_TMUX")
if [ -n "$output" ]; then
fail "installer was not silent with tmux present ($home_name): $output"
fi
done
# ── the roster declares something other than tmux ────────────────────────────
# The roster is read, not assumed. A host that declares a different transport
# is told about the binary it actually needs, and never about tmux — being sent
# to install the wrong package is worse than no advice at all.
home=$(make_home other-transport zellij)
output=$(run_doctor_check "$home" "$PATH_WITH_TMUX")
echo "$output" | grep -qF 'zellij' || fail "doctor ignored the roster's declared transport"
if echo "$output" | grep -qF 'tmux'; then
fail "doctor named tmux for a host whose roster declares zellij"
fi
output=$(run_installer_check "$home" "$PATH_WITH_TMUX")
echo "$output" | grep -qF 'zellij' || fail "installer ignored the roster's declared transport"
if echo "$output" | grep -qF 'tmux'; then
fail "installer named tmux for a host whose roster declares zellij"
fi
# ── a quoted or trailing-comment transport value ─────────────────────────────
# YAML permits both and neither is exotic; a check that installs `tmux"` or
# reads `tmux # default` as a binary name would send the operator nowhere.
home=$(make_home quoted-transport '"tmux" # the only transport today')
output=$(run_doctor_check "$home" "$PATH_WITH_TMUX")
echo "$output" | grep -qF '[OK] Fleet transport available: tmux' ||
fail "doctor did not parse a quoted/commented transport value: $output"
output=$(run_installer_check "$home" "$PATH_WITH_TMUX")
if [ -n "$output" ]; then
fail "installer did not parse a quoted/commented transport value: $output"
fi
echo "ok - fleet transport checks (mosaic-doctor + install.sh)"
+1 -1
View File
@@ -25,7 +25,7 @@
"lint": "eslint src",
"typecheck": "tsc --noEmit",
"test": "vitest run --passWithNoTests && pnpm run test:framework-shell",
"test:framework-shell": "bash framework/tools/quality/scripts/check-test-enumeration.sh && bash framework/tools/quality/scripts/test-check-test-enumeration.sh && python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/promotion_binding_unittest.py && python3 src/lease-broker/promotion_trigger_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/receipt_observer_client_unittest.py && python3 src/lease-broker/invariant_r_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 src/mutator-gate/version_coupling_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-pr-review-repo-host-override.sh && bash framework/tools/git/test-ci-queue-wait-branch-absent.sh && bash framework/tools/git/test-ci-queue-wait-tristate.sh && bash framework/tools/git/test-ci-queue-wait-github-checks.sh && bash framework/tools/git/test-pr-merge-queue-branch.sh && bash framework/tools/git/test-pr-merge-head-pin.sh && bash framework/tools/git/test-pr-merge-message-field.sh && bash framework/tools/git/test-git-credential-mosaic.sh && bash framework/tools/git/test-gitea-token-identity.sh && bash framework/tools/woodpecker/test-terminal-green-contract.sh && bash framework/tools/_scripts/test-install-ordering-guard.sh && bash framework/tools/_scripts/test-mosaic-init-rce.sh && bash framework/tools/tmux/agent-send.test.sh && bash framework/tools/wake/test-wake-store-ack.sh && bash framework/tools/wake/test-wake-store-enqueue-race.sh && bash framework/tools/wake/test-wake-digest-hmac.sh && bash framework/tools/wake/test-wake-digest-quarantine.sh && bash framework/tools/wake/test-wake-detector.sh && bash framework/tools/wake/test-wake-fn-oracle.sh && bash framework/tools/wake/test-wake-reconcile.sh && bash framework/tools/wake/test-wake-beacon.sh && bash framework/tools/wake/test-wake-preimage.sh && bash framework/tools/wake/test-wake-install.sh"
"test:framework-shell": "bash framework/tools/quality/scripts/check-test-enumeration.sh && bash framework/tools/quality/scripts/test-check-test-enumeration.sh && python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/promotion_binding_unittest.py && python3 src/lease-broker/promotion_trigger_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/receipt_observer_client_unittest.py && python3 src/lease-broker/invariant_r_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 src/mutator-gate/version_coupling_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-pr-review-repo-host-override.sh && bash framework/tools/git/test-ci-queue-wait-branch-absent.sh && bash framework/tools/git/test-ci-queue-wait-tristate.sh && bash framework/tools/git/test-ci-queue-wait-github-checks.sh && bash framework/tools/git/test-pr-merge-queue-branch.sh && bash framework/tools/git/test-pr-merge-head-pin.sh && bash framework/tools/git/test-pr-merge-message-field.sh && bash framework/tools/git/test-git-credential-mosaic.sh && bash framework/tools/git/test-gitea-token-identity.sh && bash framework/tools/woodpecker/test-terminal-green-contract.sh && bash framework/tools/_scripts/test-install-ordering-guard.sh && bash framework/tools/_scripts/test-mosaic-init-rce.sh && bash framework/tools/_scripts/test-fleet-transport-check.sh && bash framework/tools/tmux/agent-send.test.sh && bash framework/tools/wake/test-wake-store-ack.sh && bash framework/tools/wake/test-wake-store-enqueue-race.sh && bash framework/tools/wake/test-wake-digest-hmac.sh && bash framework/tools/wake/test-wake-digest-quarantine.sh && bash framework/tools/wake/test-wake-detector.sh && bash framework/tools/wake/test-wake-fn-oracle.sh && bash framework/tools/wake/test-wake-reconcile.sh && bash framework/tools/wake/test-wake-beacon.sh && bash framework/tools/wake/test-wake-preimage.sh && bash framework/tools/wake/test-wake-install.sh"
},
"dependencies": {
"@mosaicstack/brain": "workspace:*",
+42
View File
@@ -309,6 +309,43 @@ require_cmd() {
fi
}
# Fleet transport binary (#1240).
#
# `mosaic fleet --help` reads "Manage the local Mosaic tmux fleet" and every
# roster the CLI scaffolds sets `transport: tmux`, but nothing in this script
# provides tmux and, until now, nothing in it mentioned tmux at all. A
# greenfield host came out of this installer able to install a fleet, start a
# fleet, and run no seat — the operator's first signal was `mosaic fleet ps`.
#
# Not a `require_cmd`: tmux is required by the fleet, not by mosaic. Plenty of
# hosts install this to run `mosaic claude` and will never scaffold a roster,
# and failing their install over a binary they do not need would be wrong. It
# is a warning that names precisely what it blocks.
#
# `tools/_scripts/mosaic-doctor` carries a deliberately parallel check, so the
# same host state gets the same answer from an audit as from an install. They
# are separate implementations because this one has to work before the
# framework's scripts are guaranteed to be on disk; keep their wording in step.
check_fleet_transport() {
local transport=tmux
local roster="$MOSAIC_HOME/fleet/roster.yaml"
local declared=""
if [[ -f "$roster" ]]; then
declared="$(sed -n 's/^[[:space:]]*transport:[[:space:]]*//p' "$roster" | head -1 |
tr -d '"'\''' | tr -d '\r' | awk '{print $1}')"
[[ -n "$declared" ]] && transport="$declared"
fi
command -v "$transport" &>/dev/null && return 0
warn "Fleet transport '$transport' is not installed."
echo " The Mosaic fleet runs its agent seats inside $transport. Without it,"
echo " ${C}mosaic fleet start${RESET} reports success and no seat comes up."
echo " Install it before using the fleet, e.g. ${C}sudo apt-get install -y $transport${RESET}"
echo " (this does not affect ${C}mosaic claude${RESET} or the other single-runtime commands)."
}
installed_cli_version() {
local json
json="$(npm ls -g --depth=0 --json --prefix="$PREFIX" 2>/dev/null)" || true
@@ -870,6 +907,11 @@ if [[ "$FLAG_CHECK" == "false" ]]; then
ok "Done."
fi
# Fleet readiness (#1240). Runs in both normal and --check mode: "what is the
# state of this host" is exactly the question --check is asked, and a host that
# cannot run a seat should not have to discover it from `fleet ps`.
check_fleet_transport
} # end main
main "$@"