All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
Second- and third-round independent-review reliability fixes on the keep-mode
upgrade rollback path, plus accurate abort messaging. All fixed red-first with
self-verifying controls in the rollback gate.
Round 2 (blockers A/B, should-fix C):
- install.sh: `trap 'restore_snapshot; exit 1' ERR INT TERM` so an INT/TERM
mid-sync terminates instead of resuming past the interrupt and reporting
success (a bash signal handler that only returns does not terminate).
- manifest.{ts,sh}: reject a degenerate [framework] section whose entries are
all empty or bare-dot (`/`, `./`, `.`, `..`) — it passed the non-empty guard
yet yielded zero usable globs, silently resolving everything to operator.
Parity via a shared `[^/.]` usable-glob test; TS throws ManifestError.
- finalize.ts: classify the sync-abort message — a ManifestError is a pre-sync
validation abort ("no files were changed"); any other error may be partial.
Round 3 (blockers D1, D2):
- install.sh: enumerate framework files with a checked temp file (_scan_or_die)
instead of `< <(find …)` — process substitution discards find's exit status,
so an EACCES/I/O failure mid-scan would truncate the file list yet leave the
loop exiting 0, committing a partial upgrade as success (ERR trap never fires).
- install.sh: guard the `rm -rf; mkdir -p` target reset inside restore_snapshot
— a bare reset failing under set -e exits silently after partial deletion,
never printing the snapshot-recovery pointer. Now checked like the cp -a
restore: on failure it preserves the snapshot and tells the operator where.
Tests: rollback gate 14→28 (Parts C/D/E with disabled-guard controls);
new finalize-sync-abort.spec.ts (3). No secret value is ever emitted; snapshots
stay 0700. Gates green: typecheck, lint, format:check, full mosaic vitest 1094,
HARD GATE 193, rollback 28, migration 21.
Refs #791
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
487 lines
22 KiB
Bash
Executable File
487 lines
22 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# -E (errtrace): the ERR trap must propagate INTO functions and command
|
|
# substitutions. Without it the `trap restore_snapshot ERR` set below is dead
|
|
# code for any failure inside sync_framework_keep() (its whole body runs in a
|
|
# function) — a mid-sync failure would abort with a half-written target and NO
|
|
# rollback (#791 B1). Keep -E first so every later function inherits the trap.
|
|
set -Eeuo pipefail
|
|
|
|
# ─── Mosaic Framework Installer ──────────────────────────────────────────────
|
|
#
|
|
# Installs/upgrades the framework DATA to ~/.config/mosaic/.
|
|
# No executables are placed on PATH — the mosaic npm CLI is the only binary.
|
|
#
|
|
# Called by tools/install.sh (the unified installer). Can also be run directly.
|
|
#
|
|
# Environment:
|
|
# MOSAIC_HOME — target directory (default: ~/.config/mosaic)
|
|
# MOSAIC_INSTALL_MODE — prompt|keep|overwrite (default: prompt)
|
|
# MOSAIC_ALLOW_MISSING_SEQUENTIAL_THINKING — 1 to bypass MCP check
|
|
# MOSAIC_SKIP_SKILLS_SYNC — 1 to skip skill sync
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
|
|
SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
TARGET_DIR="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
|
INSTALL_MODE="${MOSAIC_INSTALL_MODE:-prompt}"
|
|
|
|
# Shared framework path-ownership manifest reader (#791). Parity with
|
|
# packages/mosaic/src/framework/manifest.ts — both consume framework-manifest.txt.
|
|
# Sourcing does not run its CLI dispatch (guarded by BASH_SOURCE==$0).
|
|
# shellcheck source=tools/_lib/manifest.sh
|
|
source "$SOURCE_DIR/tools/_lib/manifest.sh"
|
|
|
|
# Which paths a keep-mode upgrade may touch is no longer a hand-maintained
|
|
# denylist. It is derived from the shared framework-manifest.txt (#791): the
|
|
# updater only ever creates/overwrites framework-owned paths and only prunes a
|
|
# retired framework file inside a shipped framework subtree. Everything else —
|
|
# every operator file, and every path the manifest never anticipated — is
|
|
# operator-owned by default (fail-safe) and is never written or deleted. See
|
|
# sync_framework_keep() below and packages/mosaic/src/framework/manifest.ts.
|
|
|
|
# Framework-owned contract files: re-copied from defaults/ on every upgrade (the
|
|
# user must not edit them; a divergent copy is backed up once before overwrite).
|
|
# USER_SEEDED files are written once on first install, then owned by the user.
|
|
# Both lists are APPEND-FRIENDLY — add a new shipped framework file here and to the
|
|
# matching list in packages/mosaic/src/config/file-adapter.ts.
|
|
FRAMEWORK_OWNED=("CONSTITUTION.md" "AGENTS.md" "STANDARDS.md")
|
|
USER_SEEDED=("TOOLS.md")
|
|
|
|
# Current framework schema version — bump this when the layout changes.
|
|
# The migration system uses this to run upgrade steps.
|
|
FRAMEWORK_VERSION=3
|
|
|
|
# ─── colours ──────────────────────────────────────────────────────────────────
|
|
if [[ -t 1 ]]; then
|
|
GREEN='\033[0;32m' YELLOW='\033[0;33m' RED='\033[0;31m'
|
|
CYAN='\033[0;36m' BOLD='\033[1m' RESET='\033[0m'
|
|
else
|
|
GREEN='' YELLOW='' RED='' CYAN='' BOLD='' RESET=''
|
|
fi
|
|
|
|
ok() { echo -e " ${GREEN}✓${RESET} $1"; }
|
|
warn() { echo -e " ${YELLOW}⚠${RESET} $1" >&2; }
|
|
fail() { echo -e " ${RED}✗${RESET} $1" >&2; }
|
|
step() { echo -e "\n${BOLD}$1${RESET}"; }
|
|
|
|
# ─── snapshot / restore (crash safety for upgrades) ──────────────────────────
|
|
SNAPSHOT_DIR=""
|
|
make_snapshot() {
|
|
is_existing_install || return 0
|
|
# mktemp -d creates the dir 0700 — the snapshot (which mirrors operator config,
|
|
# possibly including secrets) is never world-readable.
|
|
SNAPSHOT_DIR="$(mktemp -d "${TMPDIR:-/tmp}/mosaic-snapshot-XXXXXX")"
|
|
# The snapshot MUST be complete: restore rebuilds the target from it, so a
|
|
# partial capture (unreadable file, disk-full, I/O error) would silently
|
|
# discard whatever it missed. If cp -a cannot copy the whole tree, abort NOW —
|
|
# before the restore trap is armed and before anything is mutated. Fail closed
|
|
# rather than proceed with a snapshot we cannot trust (#791 blocker-2).
|
|
if ! cp -a "$TARGET_DIR/." "$SNAPSHOT_DIR/"; then
|
|
fail "Could not capture a complete pre-upgrade snapshot of $TARGET_DIR — aborting before any changes were made (fail-closed)."
|
|
rm -rf "$SNAPSHOT_DIR"; SNAPSHOT_DIR=""
|
|
exit 1
|
|
fi
|
|
}
|
|
restore_snapshot() {
|
|
# Disarm the trap first: restore runs under `set -e`, and a non-zero step
|
|
# inside it must not re-enter this handler (errtrace makes ERR fire in
|
|
# functions now). One restore attempt, then let the script exit non-zero.
|
|
trap - ERR INT TERM
|
|
[[ -n "$SNAPSHOT_DIR" && -d "$SNAPSHOT_DIR" ]] || return 0
|
|
fail "Install interrupted/failed — restoring previous state from snapshot"
|
|
# Reset the target before rebuilding from the snapshot — but CHECK it. Under
|
|
# `set -e` (trap already disarmed) a bare `rm -rf; mkdir -p` that fails would
|
|
# exit the whole script immediately, after `rm` may have deleted part of the
|
|
# target, WITHOUT ever printing the recovery pointer below — the operator would
|
|
# be left with a half-removed target and no idea the snapshot survives in /tmp.
|
|
# Test the reset explicitly (like the cp -a below), and on failure keep the
|
|
# snapshot and tell the operator where it is (#791 blocker-D2).
|
|
if ! rm -rf "$TARGET_DIR" || ! mkdir -p "$TARGET_DIR"; then
|
|
fail "Snapshot restore could not reset $TARGET_DIR. Your previous configuration is preserved at: $SNAPSHOT_DIR — copy it back into $TARGET_DIR manually."
|
|
return 1
|
|
fi
|
|
# Surface an incomplete restore instead of swallowing it: the snapshot is the
|
|
# last good copy, so if cp cannot fully rebuild the target we must NOT delete
|
|
# the snapshot — point the operator at it for manual recovery (#791 blocker-2).
|
|
if ! cp -a "$SNAPSHOT_DIR/." "$TARGET_DIR/"; then
|
|
fail "Snapshot restore did not complete cleanly. Your previous configuration is preserved at: $SNAPSHOT_DIR — copy it back into $TARGET_DIR manually."
|
|
return 1
|
|
fi
|
|
}
|
|
cleanup_snapshot() { [[ -n "$SNAPSHOT_DIR" && -d "$SNAPSHOT_DIR" ]] && rm -rf "$SNAPSHOT_DIR"; SNAPSHOT_DIR=""; }
|
|
|
|
# Reconcile contract files after sync: framework-owned overwrite (backup-once),
|
|
# user-seeded seed-if-absent.
|
|
reconcile_framework_files() {
|
|
local defaults="$TARGET_DIR/defaults" f
|
|
[[ -d "$defaults" ]] || return 0
|
|
for f in "${FRAMEWORK_OWNED[@]}"; do
|
|
[[ -f "$defaults/$f" ]] || continue
|
|
# Already current — skip to avoid mtime churn.
|
|
if [[ -f "$TARGET_DIR/$f" ]] && cmp -s "$TARGET_DIR/$f" "$defaults/$f"; then
|
|
continue
|
|
fi
|
|
if [[ -f "$TARGET_DIR/$f" && ! -f "$TARGET_DIR/${f}.pre-constitution.bak" ]]; then
|
|
cp "$TARGET_DIR/$f" "$TARGET_DIR/${f}.pre-constitution.bak"
|
|
warn "$f is now framework-owned and was updated; your previous copy is saved as ${f}.pre-constitution.bak — re-apply intended changes as a .local overlay or policy/ file (see CONSTITUTION.md / constitution/LAYER-MODEL.md)."
|
|
fi
|
|
cp "$defaults/$f" "$TARGET_DIR/$f"
|
|
done
|
|
for f in "${USER_SEEDED[@]}"; do
|
|
[[ -f "$defaults/$f" ]] || continue
|
|
if [[ ! -f "$TARGET_DIR/$f" ]]; then
|
|
cp "$defaults/$f" "$TARGET_DIR/$f"
|
|
ok "Seeded $f from defaults"
|
|
fi
|
|
done
|
|
}
|
|
|
|
# ─── helpers ──────────────────────────────────────────────────────────────────
|
|
|
|
is_existing_install() {
|
|
[[ -d "$TARGET_DIR" ]] || return 1
|
|
[[ -f "$TARGET_DIR/AGENTS.md" || -f "$TARGET_DIR/SOUL.md" ]]
|
|
}
|
|
|
|
installed_framework_version() {
|
|
local vf="$TARGET_DIR/.framework-version"
|
|
if [[ -f "$vf" ]]; then
|
|
cat "$vf" 2>/dev/null || echo "0"
|
|
else
|
|
# No version file = legacy install (version 0 or 1)
|
|
if [[ -d "$TARGET_DIR/bin" ]]; then
|
|
echo "1" # Has bin/ → pre-migration legacy
|
|
else
|
|
echo "0" # Fresh or unknown
|
|
fi
|
|
fi
|
|
}
|
|
|
|
write_framework_version() {
|
|
echo "$FRAMEWORK_VERSION" > "$TARGET_DIR/.framework-version"
|
|
}
|
|
|
|
select_install_mode() {
|
|
case "$INSTALL_MODE" in
|
|
keep|overwrite|prompt) ;;
|
|
*)
|
|
fail "Invalid MOSAIC_INSTALL_MODE='$INSTALL_MODE'. Use: prompt, keep, overwrite."
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
if ! is_existing_install; then
|
|
INSTALL_MODE="overwrite"
|
|
return
|
|
fi
|
|
|
|
case "$INSTALL_MODE" in
|
|
keep|overwrite) ;;
|
|
prompt)
|
|
if [[ -t 0 ]]; then
|
|
echo ""
|
|
echo "Existing Mosaic install detected at: $TARGET_DIR"
|
|
echo " 1) keep Update framework, preserve local files (SOUL.md, USER.md, etc.)"
|
|
echo " 2) overwrite Replace everything"
|
|
echo " 3) cancel Abort"
|
|
printf "Selection [1/2/3] (default: 1): "
|
|
read -r selection
|
|
case "${selection:-1}" in
|
|
1|k|K|keep) INSTALL_MODE="keep" ;;
|
|
2|o|O|overwrite) INSTALL_MODE="overwrite" ;;
|
|
*) fail "Install cancelled."; exit 1 ;;
|
|
esac
|
|
else
|
|
INSTALL_MODE="keep"
|
|
fi
|
|
;;
|
|
esac
|
|
}
|
|
|
|
sync_framework() {
|
|
local source_real target_real
|
|
source_real="$(cd "$SOURCE_DIR" && pwd -P)"
|
|
target_real="$(mkdir -p "$TARGET_DIR" && cd "$TARGET_DIR" && pwd -P)"
|
|
|
|
if [[ "$source_real" == "$target_real" ]]; then
|
|
warn "Source and target are the same directory; skipping file sync."
|
|
return
|
|
fi
|
|
|
|
if [[ "$INSTALL_MODE" == "keep" ]]; then
|
|
# The `mosaic update` path. Manifest-driven, never-deleting-outside-framework:
|
|
# operator config is structurally protected (#791). No rsync --delete here.
|
|
# The manifest is already loaded+validated in main() BEFORE the snapshot/trap
|
|
# (a fail-closed manifest must abort without ever restoring over operator
|
|
# files — see the pre-flight in main, #791 blocker-1).
|
|
sync_framework_keep
|
|
return
|
|
fi
|
|
|
|
# overwrite mode — a full replace, chosen only for a fresh install or when the
|
|
# operator explicitly asks to replace everything. No operator state to protect.
|
|
sync_framework_overwrite
|
|
}
|
|
|
|
# Enumerate a NUL-delimited file list via `find` into the temp file $1, failing
|
|
# CLOSED if find errors. We capture to a checked file instead of consuming
|
|
# `< <(find …)` directly because a process substitution discards the producer's
|
|
# exit status: an EACCES/I/O failure partway through a scan would truncate the
|
|
# list yet leave the reading `while` loop exiting 0, so a partial upgrade would
|
|
# commit and report success and the ERR/restore trap would never fire. Running
|
|
# find to completion first, then checking its status, turns that silent
|
|
# truncation into a fail-closed abort that the restore trap can act on (#791
|
|
# blocker-D1). $1 after the shift is the scan root — named in the error.
|
|
_scan_or_die() {
|
|
local out="$1"; shift
|
|
if ! find "$@" -print0 > "$out"; then
|
|
fail "Could not enumerate framework files under '$1' — aborting before committing an incomplete sync (fail-closed)."
|
|
return 1 # D1-GUARD
|
|
fi
|
|
}
|
|
|
|
# Keep-mode sync: create/refresh framework-owned files and prune only retired
|
|
# framework files inside shipped framework subtrees. Operator-owned and unknown
|
|
# paths (fail-safe default) are never written and never deleted — the #791 HARD
|
|
# GATE. Single code path (no rsync) so it is byte-for-byte parity-testable.
|
|
sync_framework_keep() {
|
|
local src="$SOURCE_DIR" dst="$TARGET_DIR" abs rel root list
|
|
|
|
# 1) Overlay copy — every framework-owned source file, refreshed only when its
|
|
# bytes changed (no mtime churn on unchanged files, never on operator files).
|
|
# The source scan is captured fail-closed (#791 blocker-D1): a find failure
|
|
# aborts the sync (→ ERR trap → restore) rather than silently truncating it.
|
|
list="$(mktemp)"
|
|
_scan_or_die "$list" "$src" -type f || { rm -f "$list"; return 1; }
|
|
while IFS= read -r -d '' abs; do
|
|
rel="${abs#"$src"/}"
|
|
case "$rel" in
|
|
.git|.git/*|.framework-version|*.pre-constitution.bak) continue ;;
|
|
esac
|
|
manifest_is_framework "$rel" || continue
|
|
if [[ -f "$dst/$rel" ]] && cmp -s "$abs" "$dst/$rel"; then continue; fi
|
|
[[ "$rel" == */* ]] && mkdir -p "$dst/${rel%/*}"
|
|
cp "$abs" "$dst/$rel"
|
|
done < "$list"
|
|
rm -f "$list"
|
|
|
|
# 2) Scoped prune — within each shipped framework subtree root, remove
|
|
# framework-owned target files the current source no longer ships. Operator
|
|
# carve-outs (e.g. tools/_lib/credentials.json) resolve to operator and are
|
|
# skipped; unknown paths resolve to operator too — both are unreachable here.
|
|
# Each subtree scan is captured fail-closed for the same reason as the copy.
|
|
while IFS= read -r root; do
|
|
[[ -n "$root" && -d "$dst/$root" ]] || continue
|
|
list="$(mktemp)"
|
|
_scan_or_die "$list" "$dst/$root" -type f || { rm -f "$list"; return 1; }
|
|
while IFS= read -r -d '' abs; do
|
|
rel="${abs#"$dst"/}"
|
|
case "$rel" in *.pre-constitution.bak) continue ;; esac
|
|
[[ -f "$src/$rel" ]] && continue # still shipped
|
|
manifest_is_framework "$rel" || continue
|
|
rm -f "$abs"
|
|
done < "$list"
|
|
rm -f "$list"
|
|
# Drop framework dirs left empty by the prune (never touches a dir that still
|
|
# holds an operator file — those are never emptied). A genuine find failure
|
|
# (unreadable dir) is surfaced as a warning rather than silently swallowed;
|
|
# the "directory not empty" races we tolerate are ignored via -delete's own
|
|
# rc, not by hiding stderr — so a real error is still visible to the operator.
|
|
if ! find "$dst/$root" -type d -empty -delete 2>/dev/null; then
|
|
warn "prune: could not fully sweep empty framework dirs under $root (left as-is)"
|
|
fi
|
|
done < <(manifest_subtree_roots)
|
|
}
|
|
|
|
# Overwrite-mode sync: full replace. Only reached for a fresh install or an
|
|
# explicit operator "replace everything" choice, so nothing is preserved.
|
|
sync_framework_overwrite() {
|
|
if command -v rsync >/dev/null 2>&1; then
|
|
rsync -a --delete \
|
|
--exclude ".git" --exclude ".framework-version" --exclude "*.pre-constitution.bak" \
|
|
"$SOURCE_DIR/" "$TARGET_DIR/"
|
|
return
|
|
fi
|
|
find "$TARGET_DIR" -mindepth 1 -maxdepth 1 \
|
|
! -name ".git" ! -name ".framework-version" ! -name "*.pre-constitution.bak" \
|
|
-exec rm -rf {} +
|
|
cp -R "$SOURCE_DIR"/. "$TARGET_DIR"/
|
|
rm -rf "$TARGET_DIR/.git"
|
|
}
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
# Migrations — run sequentially from the installed version to FRAMEWORK_VERSION
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
run_migrations() {
|
|
local from_version
|
|
from_version="$(installed_framework_version)"
|
|
|
|
if [[ "$from_version" -ge "$FRAMEWORK_VERSION" ]]; then
|
|
return # Already current
|
|
fi
|
|
|
|
step "Running migrations (v${from_version} → v${FRAMEWORK_VERSION})"
|
|
|
|
# ── Migration: v0/v1 → v2 ─────────────────────────────────────────────────
|
|
# Remove bin/ directory — all executables now live in the npm CLI.
|
|
# Scripts that were in bin/ are now in tools/_scripts/.
|
|
if [[ "$from_version" -lt 2 ]]; then
|
|
if [[ -d "$TARGET_DIR/bin" ]]; then
|
|
ok "Removing legacy bin/ directory (executables now in npm CLI)"
|
|
rm -rf "$TARGET_DIR/bin"
|
|
fi
|
|
|
|
# Remove old mosaic PATH entry from shell profiles
|
|
for profile in "$HOME/.bashrc" "$HOME/.zshrc" "$HOME/.profile"; do
|
|
if [[ -f "$profile" ]] && grep -qF "$TARGET_DIR/bin" "$profile"; then
|
|
# Remove the PATH line and the comment above it
|
|
sed -i.mosaic-migration-bak \
|
|
-e "\|# Mosaic agent framework|d" \
|
|
-e "\|$TARGET_DIR/bin|d" \
|
|
"$profile"
|
|
ok "Cleaned up old PATH entry from $(basename "$profile")"
|
|
rm -f "${profile}.mosaic-migration-bak"
|
|
fi
|
|
done
|
|
|
|
# Remove stale rails/ symlink
|
|
if [[ -L "$TARGET_DIR/rails" ]]; then
|
|
rm -f "$TARGET_DIR/rails"
|
|
fi
|
|
fi
|
|
|
|
# ── Migration: v2 → v3 (Constitution split) ───────────────────────────────
|
|
# CONSTITUTION.md / AGENTS.md / STANDARDS.md become framework-owned (overwritten
|
|
# on upgrade). reconcile_framework_files() has already run before this point: it
|
|
# backed up any user-edited copy to <file>.pre-constitution.bak and installed the
|
|
# new framework version. Nothing further to do here — the advisory was emitted at
|
|
# reconcile time. (STANDARDS.local.md composition lands with the overlay composer.)
|
|
if [[ "$from_version" -lt 3 ]]; then
|
|
ok "Migrated to the Constitution layout (framework-owned CONSTITUTION/AGENTS/STANDARDS)"
|
|
fi
|
|
}
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
# Main
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
step "Installing Mosaic framework"
|
|
|
|
mkdir -p "$TARGET_DIR"
|
|
select_install_mode
|
|
|
|
if [[ "$INSTALL_MODE" == "keep" ]]; then
|
|
ok "Install mode: keep local files (SOUL.md, USER.md, TOOLS.md, memory/)"
|
|
else
|
|
ok "Install mode: overwrite"
|
|
fi
|
|
|
|
# Pre-flight (keep mode): load + validate the framework manifest BEFORE taking a
|
|
# snapshot or arming the restore trap. A fail-closed manifest (missing / empty /
|
|
# malformed) must abort here WITHOUT deleting or restoring over operator files —
|
|
# the snapshot/restore path exists only for a genuine mid-sync mutation failure,
|
|
# not for a validation failure that has touched nothing yet (#791 blocker-1).
|
|
if [[ "$INSTALL_MODE" == "keep" ]]; then
|
|
manifest_load
|
|
fi
|
|
|
|
# Snapshot before any destructive file operation; restore on interrupt/failure.
|
|
# The trap MUST exit after restoring: a bash INT/TERM handler that merely returns
|
|
# does NOT terminate the script — execution would resume past the interrupt,
|
|
# clear the snapshot, and report success, leaving a partial post-interrupt update
|
|
# (#791 blocker-A). `restore_snapshot; exit 1` guarantees a non-zero exit for
|
|
# both the errtrace (ERR) and signal (INT/TERM) paths.
|
|
make_snapshot
|
|
trap 'restore_snapshot; exit 1' ERR INT TERM
|
|
|
|
sync_framework
|
|
|
|
# Ensure persistent directories exist
|
|
mkdir -p "$TARGET_DIR/memory"
|
|
mkdir -p "$TARGET_DIR/credentials"
|
|
|
|
# Reconcile contract files from defaults/ into the framework root: framework-owned
|
|
# files (CONSTITUTION/AGENTS/STANDARDS) are overwritten every upgrade (a divergent
|
|
# copy is backed up once); user-seeded files (TOOLS) are written on first install only.
|
|
#
|
|
# This list must match the framework-contract whitelist in
|
|
# packages/mosaic/src/config/file-adapter.ts (FileConfigAdapter.syncFramework).
|
|
# SOUL.md and USER.md are intentionally NOT seeded here — they are generated
|
|
# by `mosaic init` from templates with user-supplied values.
|
|
reconcile_framework_files
|
|
|
|
# Ensure tool scripts are executable
|
|
find "$TARGET_DIR/tools" -name "*.sh" -exec chmod +x {} + 2>/dev/null || true
|
|
find "$TARGET_DIR/tools/_scripts" -type f -exec chmod +x {} + 2>/dev/null || true
|
|
|
|
ok "Framework synced to $TARGET_DIR"
|
|
|
|
# Run migrations before post-install (migrations may remove old bin/ etc.)
|
|
run_migrations
|
|
|
|
# File-system phase complete and consistent — clear the restore trap.
|
|
trap - ERR INT TERM
|
|
cleanup_snapshot
|
|
|
|
# Testability / minimal-install hook: stop after the file-system phase, before any
|
|
# environment-touching post-install steps (runtime linking, MCP setup, skills, doctor).
|
|
if [[ "${MOSAIC_SYNC_ONLY:-0}" == "1" ]]; then
|
|
write_framework_version
|
|
ok "Sync-only mode: file phase complete"
|
|
exit 0
|
|
fi
|
|
|
|
step "Post-install tasks"
|
|
|
|
SCRIPTS="$TARGET_DIR/tools/_scripts"
|
|
|
|
if [[ -x "$SCRIPTS/mosaic-link-runtime-assets" ]]; then
|
|
if "$SCRIPTS/mosaic-link-runtime-assets" >/dev/null 2>&1; then
|
|
ok "Runtime assets linked"
|
|
else
|
|
warn "Runtime asset linking failed (non-fatal)"
|
|
fi
|
|
fi
|
|
|
|
if [[ -x "$SCRIPTS/mosaic-ensure-sequential-thinking" ]]; then
|
|
if "$SCRIPTS/mosaic-ensure-sequential-thinking" >/dev/null 2>&1; then
|
|
ok "sequential-thinking MCP configured"
|
|
else
|
|
if [[ "${MOSAIC_ALLOW_MISSING_SEQUENTIAL_THINKING:-0}" == "1" ]]; then
|
|
warn "sequential-thinking MCP setup bypassed (MOSAIC_ALLOW_MISSING_SEQUENTIAL_THINKING=1)"
|
|
else
|
|
fail "sequential-thinking MCP setup failed (hard requirement)."
|
|
exit 1
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
if [[ -x "$SCRIPTS/mosaic-ensure-excalidraw" ]]; then
|
|
"$SCRIPTS/mosaic-ensure-excalidraw" >/dev/null 2>&1 && ok "excalidraw MCP configured" || warn "excalidraw MCP setup failed (non-fatal)"
|
|
fi
|
|
|
|
if [[ "${MOSAIC_SKIP_SKILLS_SYNC:-0}" != "1" ]] && [[ -x "$SCRIPTS/mosaic-sync-skills" ]]; then
|
|
"$SCRIPTS/mosaic-sync-skills" >/dev/null 2>&1 && ok "Skills synced" || warn "Skills sync failed (non-fatal)"
|
|
fi
|
|
|
|
if [[ -x "$SCRIPTS/mosaic-migrate-local-skills" ]]; then
|
|
"$SCRIPTS/mosaic-migrate-local-skills" --apply >/dev/null 2>&1 && ok "Local skills migrated" || warn "Local skill migration failed (non-fatal)"
|
|
fi
|
|
|
|
if [[ -x "$SCRIPTS/mosaic-doctor" ]]; then
|
|
"$SCRIPTS/mosaic-doctor" >/dev/null 2>&1 && ok "Health audit passed" || warn "Health audit reported issues — run 'mosaic doctor' for details"
|
|
fi
|
|
|
|
# Write version stamp AFTER everything succeeds
|
|
write_framework_version
|
|
|
|
# ── Summary ──────────────────────────────────────────────────
|
|
echo ""
|
|
echo -e "${GREEN}${BOLD} Mosaic framework installed.${RESET}"
|
|
echo ""
|
|
|
|
if [[ ! -f "$TARGET_DIR/SOUL.md" ]]; then
|
|
echo -e " Run ${CYAN}mosaic init${RESET} to set up your agent identity."
|
|
echo ""
|
|
fi
|