feat(doctor): brain-home fleet-state check (#1298 follow-up, canon §6)
ci/woodpecker/pr/ci Pipeline was successful

mosaic-doctor now reports which tree fleet state resolves from and warns
on the drift a launch would hit at runtime:

- stale MOSAIC_BRAIN_HOME (no fleet/agents under it)
- symlinked brain/agents paths (managed-directory boundary)
- group/world-readable agents dir (0700 projection boundary)
- split state: env files in BOTH config-home and brain trees

Resolution mirrors brain-home.ts (#1298): MOSAIC_BRAIN_HOME wins;
canonical ~/.mosaic adopted only at the default config home; else legacy.

test-brain-home-check.sh: extraction-style harness (test-fleet-transport
discipline — functions pulled from the shipped script, never copied), 8
cases covering every branch; wired into test:framework-shell and covered
by the enumeration guard (population 53, all present).

Live-run verified on dragon-lin: caught two real drifts — agents dir
755 (fixed locally) and pre-cutover split state (expected, tracked in
mosaic-brain fleet/SEAT-CUTOVER.md).
This commit is contained in:
Zane
2026-08-17 22:34:22 -05:00
parent a80bae950d
commit bf8510879f
3 changed files with 173 additions and 1 deletions
@@ -255,6 +255,68 @@ fleet_declared_transport() {
printf '%s\n' "${declared:-tmux}"
}
# Brain-home fleet-state resolution (#1298; canon STRUCTURE-CANON §2).
#
# Seat launch envs, roles.local overrides, and profile working copies resolve
# from the brain home when one is active; roster, baseline roles, run/, and
# services stay under MOSAIC_HOME. This check surfaces which tree fleet state
# resolves from and the drift a launch would otherwise hit at runtime:
#
# - a stale MOSAIC_BRAIN_HOME pointing at a directory with no fleet/agents is a
# misconfiguration the resolver honors (explicit wins) — warn, don't pass;
# - a symlinked brain or agents dir defeats the managed-directory boundary;
# - a group/world-readable agents dir violates the 0700 projection boundary;
# - env files left in the config-home tree while a brain is active are split
# state — the write path rejects NEW split writes, but nothing would ever
# tell the operator the old files are stranded.
resolve_brain_home() {
local explicit="${MOSAIC_BRAIN_HOME:-}"
if [[ -n "$(printf '%s' "$explicit" | tr -d '[:space:]')" ]]; then
printf '%s' "$explicit"
return
fi
if [[ "$(cd "$MOSAIC_HOME" 2>/dev/null && pwd -P)" == "$HOME/.config/mosaic" \
&& -d "$HOME/.mosaic/fleet/agents" ]]; then
printf '%s' "$HOME/.mosaic"
return
fi
printf '%s' "$MOSAIC_HOME"
}
check_brain_home() {
local brain agents mode
brain="$(resolve_brain_home)"
if [[ "$brain" == "$MOSAIC_HOME" ]]; then
pass "Fleet state home: $MOSAIC_HOME (legacy single-tree; no brain adopted)"
return
fi
agents="$brain/fleet/agents"
if [[ ! -d "$agents" ]]; then
warn "Brain home '$brain' has no fleet/agents — seat envs will not resolve from it. Point MOSAIC_BRAIN_HOME at a brain carrying fleet/agents, or unset it."
return
fi
if [[ -L "$brain" || -L "$agents" ]]; then
warn "Brain fleet-state path resolves through a symlink ($brain) — the managed-directory boundary requires regular directories."
return
fi
mode="$(stat -c '%a' -- "$agents" 2>/dev/null)" || mode=""
if [[ -n "$mode" ]] && (( (8#$mode & 8#077) != 0 )); then
warn "Brain agents dir '$agents' is group/world-accessible (mode $mode) — the projection boundary requires 0700."
return
fi
if [[ -d "$MOSAIC_HOME/fleet/agents" ]] \
&& ls "$MOSAIC_HOME/fleet/agents/"*.env* >/dev/null 2>&1; then
warn "Fleet env files exist in BOTH trees — brain '$brain' is active but '$MOSAIC_HOME/fleet/agents' still carries env files (split state). Migrate them (mosaic fleet regen) and remove the config-home copies."
return
fi
pass "Fleet state home: $brain (brain active); roster + templates: $MOSAIC_HOME"
}
check_fleet_transport() {
local transport
transport="$(fleet_declared_transport)"
@@ -273,6 +335,8 @@ check_fleet_transport() {
check_fleet_transport
check_brain_home
# Legacy migration surfaces should no longer contain symlink trees.
legacy_paths=(
"$HOME/.claude/agent-guides"