#!/usr/bin/env bash set -euo pipefail MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}" FAIL_ON_WARN=0 VERBOSE=0 usage() { cat <&2 usage >&2 exit 1 ;; esac done 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/bin/mosaic-link-runtime-assets" expect_file "$MOSAIC_HOME/bin/mosaic-ensure-sequential-thinking" expect_file "$MOSAIC_HOME/bin/mosaic-sync-skills" expect_file "$MOSAIC_HOME/bin/mosaic-projects" expect_file "$MOSAIC_HOME/bin/mosaic-quality-apply" expect_file "$MOSAIC_HOME/bin/mosaic-quality-verify" expect_file "$MOSAIC_HOME/bin/mosaic-orchestrator-run" expect_file "$MOSAIC_HOME/bin/mosaic-orchestrator-sync-tasks" expect_file "$MOSAIC_HOME/bin/mosaic-orchestrator-drain" expect_file "$MOSAIC_HOME/bin/mosaic-orchestrator-matrix-publish" expect_file "$MOSAIC_HOME/bin/mosaic-orchestrator-matrix-consume" expect_file "$MOSAIC_HOME/bin/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" 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/bin/mosaic-ensure-sequential-thinking" ]]; then if "$MOSAIC_HOME/bin/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 # 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"; 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" ) 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 echo "[mosaic-doctor] warnings=$warn_count" if [[ $FAIL_ON_WARN -eq 1 && $warn_count -gt 0 ]]; then exit 1 fi