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.
484 lines
16 KiB
Bash
Executable File
484 lines
16 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
|
FAIL_ON_WARN=0
|
|
VERBOSE=0
|
|
FIX_MODE=0
|
|
|
|
usage() {
|
|
cat <<USAGE
|
|
Usage: $(basename "$0") [options]
|
|
|
|
Audit Mosaic runtime state and detect drift across agent runtimes.
|
|
|
|
Options:
|
|
--fix Auto-fix: create missing dirs, wire skills into all harnesses
|
|
--fail-on-warn Exit non-zero when warnings are found
|
|
--verbose Print pass checks too
|
|
-h, --help Show help
|
|
USAGE
|
|
}
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--fix)
|
|
FIX_MODE=1
|
|
shift
|
|
;;
|
|
--fail-on-warn)
|
|
FAIL_ON_WARN=1
|
|
shift
|
|
;;
|
|
--verbose)
|
|
VERBOSE=1
|
|
shift
|
|
;;
|
|
-h|--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
*)
|
|
echo "Unknown argument: $1" >&2
|
|
usage >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
fix_count=0
|
|
fix() { fix_count=$((fix_count + 1)); echo "[FIX] $*"; }
|
|
|
|
warn_count=0
|
|
warn() { warn_count=$((warn_count + 1)); echo "[WARN] $*"; }
|
|
pass() {
|
|
if [[ $VERBOSE -eq 1 ]]; then
|
|
echo "[OK] $*"
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
expect_dir() {
|
|
local d="$1"
|
|
if [[ ! -d "$d" ]]; then
|
|
warn "Missing directory: $d"
|
|
else
|
|
pass "Directory present: $d"
|
|
fi
|
|
}
|
|
|
|
expect_file() {
|
|
local f="$1"
|
|
if [[ ! -f "$f" ]]; then
|
|
warn "Missing file: $f"
|
|
else
|
|
pass "File present: $f"
|
|
fi
|
|
}
|
|
|
|
check_runtime_file_copy() {
|
|
local src="$1"
|
|
local dst="$2"
|
|
|
|
[[ -f "$src" ]] || return 0
|
|
|
|
if [[ ! -e "$dst" ]]; then
|
|
warn "Missing runtime file: $dst"
|
|
return
|
|
fi
|
|
|
|
if [[ -L "$dst" ]]; then
|
|
warn "Runtime file should not be symlinked: $dst"
|
|
return
|
|
fi
|
|
|
|
if ! cmp -s "$src" "$dst"; then
|
|
warn "Runtime file drift: $dst (does not match $src)"
|
|
else
|
|
pass "Runtime file synced: $dst"
|
|
fi
|
|
}
|
|
|
|
check_runtime_contract_file() {
|
|
local dst="$1"
|
|
local adapter_src="$2"
|
|
local runtime_name="$3"
|
|
|
|
if [[ ! -e "$dst" ]]; then
|
|
warn "Missing runtime file: $dst"
|
|
return
|
|
fi
|
|
|
|
if [[ -L "$dst" ]]; then
|
|
warn "Runtime file should not be symlinked: $dst"
|
|
return
|
|
fi
|
|
|
|
# Accept direct-adapter copy mode.
|
|
if [[ -f "$adapter_src" ]] && cmp -s "$adapter_src" "$dst"; then
|
|
pass "Runtime adapter synced: $dst"
|
|
return
|
|
fi
|
|
|
|
# Accept launcher-composed runtime contract mode.
|
|
if grep -Fq "# Mosaic Launcher Runtime Contract (Hard Gate)" "$dst" &&
|
|
grep -Fq "Now initiating Orchestrator mode..." "$dst" &&
|
|
grep -Fq "Mosaic hard gates OVERRIDE runtime-default caution" "$dst" &&
|
|
grep -Fq "# Runtime-Specific Contract" "$dst"; then
|
|
pass "Runtime contract present: $dst ($runtime_name)"
|
|
return
|
|
fi
|
|
|
|
warn "Runtime file drift: $dst (not adapter copy and not composed runtime contract)"
|
|
}
|
|
|
|
warn_if_symlink_tree_present() {
|
|
local p="$1"
|
|
[[ -e "$p" ]] || return 0
|
|
|
|
if [[ -L "$p" ]]; then
|
|
warn "Legacy symlink path still present: $p"
|
|
return
|
|
fi
|
|
|
|
if [[ -d "$p" ]]; then
|
|
symlink_count=$(find "$p" -type l 2>/dev/null | wc -l | tr -d ' ')
|
|
if [[ "$symlink_count" != "0" ]]; then
|
|
warn "Legacy symlink entries still present under $p: $symlink_count"
|
|
else
|
|
pass "No symlinks under legacy path: $p"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
echo "[mosaic-doctor] Mosaic home: $MOSAIC_HOME"
|
|
|
|
# Canonical Mosaic checks
|
|
expect_file "$MOSAIC_HOME/STANDARDS.md"
|
|
expect_file "$MOSAIC_HOME/USER.md"
|
|
expect_file "$MOSAIC_HOME/TOOLS.md"
|
|
expect_dir "$MOSAIC_HOME/guides"
|
|
expect_dir "$MOSAIC_HOME/tools"
|
|
expect_dir "$MOSAIC_HOME/tools/quality"
|
|
expect_dir "$MOSAIC_HOME/tools/orchestrator-matrix"
|
|
expect_dir "$MOSAIC_HOME/profiles"
|
|
expect_dir "$MOSAIC_HOME/templates/agent"
|
|
expect_dir "$MOSAIC_HOME/skills"
|
|
expect_dir "$MOSAIC_HOME/skills-local"
|
|
expect_file "$MOSAIC_HOME/tools/_scripts/mosaic-link-runtime-assets"
|
|
expect_file "$MOSAIC_HOME/tools/_scripts/mosaic-ensure-sequential-thinking"
|
|
expect_file "$MOSAIC_HOME/tools/_scripts/mosaic-sync-skills"
|
|
expect_file "$MOSAIC_HOME/tools/_scripts/mosaic-projects"
|
|
expect_file "$MOSAIC_HOME/tools/_scripts/mosaic-quality-apply"
|
|
expect_file "$MOSAIC_HOME/tools/_scripts/mosaic-quality-verify"
|
|
expect_file "$MOSAIC_HOME/tools/_scripts/mosaic-orchestrator-run"
|
|
expect_file "$MOSAIC_HOME/tools/_scripts/mosaic-orchestrator-sync-tasks"
|
|
expect_file "$MOSAIC_HOME/tools/_scripts/mosaic-orchestrator-drain"
|
|
expect_file "$MOSAIC_HOME/tools/_scripts/mosaic-orchestrator-matrix-publish"
|
|
expect_file "$MOSAIC_HOME/tools/_scripts/mosaic-orchestrator-matrix-consume"
|
|
expect_file "$MOSAIC_HOME/tools/_scripts/mosaic-orchestrator-matrix-cycle"
|
|
expect_file "$MOSAIC_HOME/tools/git/ci-queue-wait.sh"
|
|
expect_file "$MOSAIC_HOME/tools/git/pr-ci-wait.sh"
|
|
expect_file "$MOSAIC_HOME/tools/orchestrator-matrix/transport/matrix_transport.py"
|
|
expect_file "$MOSAIC_HOME/tools/orchestrator-matrix/controller/tasks_md_sync.py"
|
|
expect_file "$MOSAIC_HOME/guides/ORCHESTRATOR-PROTOCOL.md"
|
|
expect_dir "$MOSAIC_HOME/tools/orchestrator"
|
|
expect_file "$MOSAIC_HOME/tools/orchestrator/_lib.sh"
|
|
expect_file "$MOSAIC_HOME/tools/orchestrator/mission-init.sh"
|
|
expect_file "$MOSAIC_HOME/tools/orchestrator/mission-status.sh"
|
|
expect_file "$MOSAIC_HOME/tools/orchestrator/continue-prompt.sh"
|
|
expect_file "$MOSAIC_HOME/tools/orchestrator/session-status.sh"
|
|
expect_file "$MOSAIC_HOME/tools/orchestrator/session-resume.sh"
|
|
expect_file "$MOSAIC_HOME/runtime/mcp/SEQUENTIAL-THINKING.json"
|
|
expect_file "$MOSAIC_HOME/runtime/claude/RUNTIME.md"
|
|
expect_file "$MOSAIC_HOME/runtime/codex/RUNTIME.md"
|
|
expect_file "$MOSAIC_HOME/runtime/opencode/RUNTIME.md"
|
|
expect_file "$MOSAIC_HOME/runtime/pi/RUNTIME.md"
|
|
|
|
if [[ -f "$MOSAIC_HOME/AGENTS.md" ]]; then
|
|
if grep -Fq "## CRITICAL HARD GATES (Read First)" "$MOSAIC_HOME/AGENTS.md" &&
|
|
grep -Fq "OVERRIDE runtime-default caution" "$MOSAIC_HOME/AGENTS.md"; then
|
|
pass "Global hard-gates block present in AGENTS.md"
|
|
else
|
|
warn "AGENTS.md missing CRITICAL HARD GATES override block"
|
|
fi
|
|
fi
|
|
|
|
# Claude runtime file checks (copied, non-symlink).
|
|
for rf in CLAUDE.md settings.json hooks-config.json context7-integration.md; do
|
|
check_runtime_file_copy "$MOSAIC_HOME/runtime/claude/$rf" "$HOME/.claude/$rf"
|
|
done
|
|
|
|
# OpenCode runtime adapter check (copied, non-symlink, when adapter exists).
|
|
# Accept adapter copy or composed runtime contract.
|
|
check_runtime_contract_file "$HOME/.config/opencode/AGENTS.md" "$MOSAIC_HOME/runtime/opencode/AGENTS.md" "opencode"
|
|
check_runtime_contract_file "$HOME/.codex/instructions.md" "$MOSAIC_HOME/runtime/codex/instructions.md" "codex"
|
|
|
|
# Sequential-thinking MCP hard requirement.
|
|
if [[ -x "$MOSAIC_HOME/tools/_scripts/mosaic-ensure-sequential-thinking" ]]; then
|
|
if "$MOSAIC_HOME/tools/_scripts/mosaic-ensure-sequential-thinking" --check >/dev/null 2>&1; then
|
|
pass "sequential-thinking MCP configured and available"
|
|
else
|
|
warn "sequential-thinking MCP missing or misconfigured"
|
|
fi
|
|
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"
|
|
"$HOME/.claude/scripts/git"
|
|
"$HOME/.claude/scripts/codex"
|
|
"$HOME/.claude/scripts/bootstrap"
|
|
"$HOME/.claude/scripts/cicd"
|
|
"$HOME/.claude/scripts/portainer"
|
|
"$HOME/.claude/templates"
|
|
"$HOME/.claude/presets/domains"
|
|
"$HOME/.claude/presets/tech-stacks"
|
|
"$HOME/.claude/presets/workflows"
|
|
)
|
|
for p in "${legacy_paths[@]}"; do
|
|
warn_if_symlink_tree_present "$p"
|
|
done
|
|
|
|
# Skills runtime checks (still symlinked into runtime-specific skills dirs).
|
|
for runtime_skills in "$HOME/.claude/skills" "$HOME/.codex/skills" "$HOME/.config/opencode/skills" "$HOME/.pi/agent/skills"; do
|
|
[[ -d "$runtime_skills" ]] || continue
|
|
|
|
while IFS= read -r -d '' skill; do
|
|
name="$(basename "$skill")"
|
|
[[ "$name" == .* ]] && continue
|
|
target="$runtime_skills/$name"
|
|
|
|
if [[ ! -e "$target" ]]; then
|
|
warn "Missing skill link: $target"
|
|
continue
|
|
fi
|
|
|
|
if [[ ! -L "$target" ]]; then
|
|
warn "Non-symlink skill entry: $target"
|
|
continue
|
|
fi
|
|
|
|
target_real="$(readlink -f "$target" 2>/dev/null || true)"
|
|
skill_real="$(readlink -f "$skill" 2>/dev/null || true)"
|
|
if [[ -z "$target_real" || -z "$skill_real" || "$target_real" != "$skill_real" ]]; then
|
|
warn "Drifted skill link: $target (expected -> $skill)"
|
|
else
|
|
pass "Linked skill: $target"
|
|
fi
|
|
done < <(find "$MOSAIC_HOME/skills" "$MOSAIC_HOME/skills-local" -mindepth 1 -maxdepth 1 -type d -print0)
|
|
done
|
|
|
|
# Broken links only in managed runtime skill dirs.
|
|
link_roots=(
|
|
"$HOME/.claude/skills"
|
|
"$HOME/.codex/skills"
|
|
"$HOME/.config/opencode/skills"
|
|
"$HOME/.pi/agent/skills"
|
|
)
|
|
existing_link_roots=()
|
|
for d in "${link_roots[@]}"; do
|
|
[[ -e "$d" ]] && existing_link_roots+=("$d")
|
|
done
|
|
|
|
broken_links=0
|
|
if [[ ${#existing_link_roots[@]} -gt 0 ]]; then
|
|
broken_links=$(find "${existing_link_roots[@]}" -xtype l 2>/dev/null | wc -l | tr -d ' ')
|
|
fi
|
|
if [[ "$broken_links" != "0" ]]; then
|
|
warn "Broken skill symlinks detected: $broken_links"
|
|
fi
|
|
|
|
# Pi agent skills directory check.
|
|
if [[ ! -d "$HOME/.pi/agent/skills" ]]; then
|
|
warn "Pi skills directory missing: $HOME/.pi/agent/skills"
|
|
else
|
|
pass "Pi skills directory present: $HOME/.pi/agent/skills"
|
|
fi
|
|
|
|
# Pi settings.json — check skills path is configured.
|
|
pi_settings="$HOME/.pi/agent/settings.json"
|
|
if [[ -f "$pi_settings" ]]; then
|
|
if grep -q 'skills' "$pi_settings" 2>/dev/null; then
|
|
pass "Pi settings.json has skills configuration"
|
|
else
|
|
warn "Pi settings.json missing skills array — Mosaic skills may not load"
|
|
fi
|
|
fi
|
|
|
|
# Mosaic-specific skills presence check.
|
|
mosaic_skills=(mosaic-board mosaic-forge mosaic-prdy mosaic-macp mosaic-standards mosaic-prd mosaic-setup-cicd)
|
|
for skill_name in "${mosaic_skills[@]}"; do
|
|
if [[ -d "$MOSAIC_HOME/skills/$skill_name" ]] || [[ -L "$MOSAIC_HOME/skills/$skill_name" ]]; then
|
|
pass "Mosaic skill present: $skill_name"
|
|
elif [[ -d "$MOSAIC_HOME/skills-local/$skill_name" ]]; then
|
|
pass "Mosaic skill present (local): $skill_name"
|
|
else
|
|
warn "Missing Mosaic skill: $skill_name"
|
|
fi
|
|
done
|
|
|
|
# ── --fix mode: auto-wire skills into all harness directories ──────────────
|
|
if [[ $FIX_MODE -eq 1 ]]; then
|
|
echo ""
|
|
echo "[mosaic-doctor] Running auto-fix..."
|
|
|
|
# 1. Ensure all harness skill directories exist
|
|
harness_skill_dirs=(
|
|
"$HOME/.claude/skills"
|
|
"$HOME/.codex/skills"
|
|
"$HOME/.config/opencode/skills"
|
|
"$HOME/.pi/agent/skills"
|
|
)
|
|
|
|
for hdir in "${harness_skill_dirs[@]}"; do
|
|
if [[ ! -d "$hdir" ]]; then
|
|
mkdir -p "$hdir"
|
|
fix "Created missing directory: $hdir"
|
|
fi
|
|
done
|
|
|
|
# 2. Wire all Mosaic skills (canonical + local) into every harness
|
|
skill_sources=("$MOSAIC_HOME/skills" "$MOSAIC_HOME/skills-local")
|
|
|
|
for hdir in "${harness_skill_dirs[@]}"; do
|
|
# Skip if target resolves to canonical dir (avoid self-link)
|
|
hdir_real="$(readlink -f "$hdir" 2>/dev/null || true)"
|
|
canonical_real="$(readlink -f "$MOSAIC_HOME/skills" 2>/dev/null || true)"
|
|
if [[ -n "$hdir_real" && -n "$canonical_real" && "$hdir_real" == "$canonical_real" ]]; then
|
|
continue
|
|
fi
|
|
|
|
for src_dir in "${skill_sources[@]}"; do
|
|
[[ -d "$src_dir" ]] || continue
|
|
|
|
while IFS= read -r -d '' skill_path; do
|
|
skill_name="$(basename "$skill_path")"
|
|
[[ "$skill_name" == .* ]] && continue
|
|
|
|
link_path="$hdir/$skill_name"
|
|
|
|
if [[ -L "$link_path" ]]; then
|
|
# Repoint if target differs
|
|
current_target="$(readlink -f "$link_path" 2>/dev/null || true)"
|
|
expected_target="$(readlink -f "$skill_path" 2>/dev/null || true)"
|
|
if [[ "$current_target" != "$expected_target" ]]; then
|
|
ln -sfn "$skill_path" "$link_path"
|
|
fix "Repointed skill link: $link_path -> $skill_path"
|
|
fi
|
|
elif [[ -e "$link_path" ]]; then
|
|
# Non-symlink entry — preserve runtime-specific override
|
|
continue
|
|
else
|
|
ln -s "$skill_path" "$link_path"
|
|
fix "Linked skill: $link_path -> $skill_path"
|
|
fi
|
|
done < <(find "$src_dir" -mindepth 1 -maxdepth 1 -type d -print0; find "$src_dir" -mindepth 1 -maxdepth 1 -type l -print0)
|
|
done
|
|
|
|
# Prune broken symlinks in this harness dir
|
|
while IFS= read -r -d '' broken_link; do
|
|
rm -f "$broken_link"
|
|
fix "Removed broken link: $broken_link"
|
|
done < <(find "$hdir" -mindepth 1 -maxdepth 1 -xtype l -print0 2>/dev/null)
|
|
done
|
|
|
|
# 3. Ensure Pi settings.json includes Mosaic skills path
|
|
pi_settings_dir="$HOME/.pi/agent"
|
|
pi_settings_file="$pi_settings_dir/settings.json"
|
|
mkdir -p "$pi_settings_dir"
|
|
|
|
if [[ ! -f "$pi_settings_file" ]]; then
|
|
echo '{}' > "$pi_settings_file"
|
|
fix "Created Pi settings.json: $pi_settings_file"
|
|
fi
|
|
|
|
# Add skills paths if not already present
|
|
mosaic_skills_path="$MOSAIC_HOME/skills"
|
|
mosaic_local_path="$MOSAIC_HOME/skills-local"
|
|
if ! grep -q "$mosaic_skills_path" "$pi_settings_file" 2>/dev/null; then
|
|
# Use a simple approach: read, patch, write
|
|
if command -v python3 >/dev/null 2>&1; then
|
|
python3 -c "
|
|
import json, sys
|
|
with open('$pi_settings_file', 'r') as f:
|
|
data = json.load(f)
|
|
skills = data.get('skills', [])
|
|
if not isinstance(skills, list):
|
|
skills = []
|
|
for p in ['$mosaic_skills_path', '$mosaic_local_path']:
|
|
if p not in skills:
|
|
skills.append(p)
|
|
data['skills'] = skills
|
|
with open('$pi_settings_file', 'w') as f:
|
|
json.dump(data, f, indent=2)
|
|
f.write('\\n')
|
|
" 2>/dev/null && fix "Added Mosaic skills paths to Pi settings.json"
|
|
else
|
|
warn "python3 not available — cannot patch Pi settings.json. Add manually: skills: [\"$mosaic_skills_path\", \"$mosaic_local_path\"]"
|
|
fi
|
|
fi
|
|
|
|
# 4. Run link-runtime-assets if available
|
|
if [[ -x "$MOSAIC_HOME/tools/_scripts/mosaic-link-runtime-assets" ]]; then
|
|
"$MOSAIC_HOME/tools/_scripts/mosaic-link-runtime-assets" >/dev/null 2>&1 && fix "Re-ran mosaic-link-runtime-assets"
|
|
fi
|
|
|
|
echo "[mosaic-doctor] fixes=$fix_count"
|
|
fi
|
|
|
|
echo "[mosaic-doctor] warnings=$warn_count"
|
|
if [[ $FAIL_ON_WARN -eq 1 && $warn_count -gt 0 ]]; then
|
|
exit 1
|
|
fi
|