fix(install): preserve user fleet data on re-seed + refresh active units (CRITICAL) (#632)
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
ci/woodpecker/push/ci Pipeline was successful

Co-authored-by: Jason Woltje <jason@diversecanvas.com>
Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #632.
This commit is contained in:
2026-06-22 21:38:09 +00:00
committed by jason.woltje
parent d539d61e0e
commit bf2a6745c8
9 changed files with 216 additions and 13 deletions

View File

@@ -23,7 +23,15 @@ INSTALL_MODE="${MOSAIC_INSTALL_MODE:-prompt}"
# entries (CONSTITUTION/AGENTS/STANDARDS) ARE re-applied afterward by
# reconcile_framework_files (overwrite + backup-once); the rest stay user-owned.
# User-created content in these paths survives rsync --delete.
PRESERVE_PATHS=("CONSTITUTION.md" "AGENTS.md" "SOUL.md" "USER.md" "TOOLS.md" "STANDARDS.md" "memory" "sources" "credentials")
#
# fleet/* — the framework SEEDS only fleet/examples, fleet/roles, and
# fleet/roster.schema.json (synced normally). The user's own fleet files MUST
# survive `mosaic update` (which runs this sync automatically): the active
# roster (`fleet/roster.yaml` + any other `fleet/*.yaml`), per-agent env
# (`fleet/agents/`), and heartbeat run dir (`fleet/run/`). Without these, an
# update wipes the operator's fleet. Glob entries are honored by both the rsync
# path (`--exclude`) and the glob-aware cp fallback below.
PRESERVE_PATHS=("CONSTITUTION.md" "AGENTS.md" "SOUL.md" "USER.md" "TOOLS.md" "STANDARDS.md" "memory" "sources" "credentials" "fleet/*.yaml" "fleet/agents" "fleet/run")
# 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).
@@ -179,15 +187,23 @@ sync_framework() {
return
fi
# Fallback: cp-based sync
# Fallback: cp-based sync. Glob-aware so entries like "fleet/*.yaml" preserve
# every matching user file (parity with the rsync --exclude path above).
local preserve_tmp=""
if [[ "$INSTALL_MODE" == "keep" ]]; then
preserve_tmp="$(mktemp -d "${TMPDIR:-/tmp}/mosaic-preserve-XXXXXX")"
local match rel
for path in "${PRESERVE_PATHS[@]}"; do
if [[ -e "$TARGET_DIR/$path" ]]; then
mkdir -p "$preserve_tmp/$(dirname "$path")"
cp -R "$TARGET_DIR/$path" "$preserve_tmp/$path"
fi
# Unquoted $path lets the glob expand against TARGET_DIR; nullglob makes a
# non-matching pattern vanish instead of staying literal.
shopt -s nullglob
for match in "$TARGET_DIR/"$path; do
[[ -e "$match" ]] || continue
rel="${match#"$TARGET_DIR/"}"
mkdir -p "$preserve_tmp/$(dirname "$rel")"
cp -R "$match" "$preserve_tmp/$rel"
done
shopt -u nullglob
done
fi
@@ -196,12 +212,19 @@ sync_framework() {
rm -rf "$TARGET_DIR/.git"
if [[ -n "$preserve_tmp" ]]; then
# Restore by re-globbing the SAME patterns against preserve_tmp, so each
# preserved item is restored at its own relative path (e.g. only
# fleet/roster.yaml is replaced — the freshly-synced fleet/examples stays).
for path in "${PRESERVE_PATHS[@]}"; do
if [[ -e "$preserve_tmp/$path" ]]; then
rm -rf "$TARGET_DIR/$path"
mkdir -p "$TARGET_DIR/$(dirname "$path")"
cp -R "$preserve_tmp/$path" "$TARGET_DIR/$path"
fi
shopt -s nullglob
for match in "$preserve_tmp/"$path; do
[[ -e "$match" ]] || continue
rel="${match#"$preserve_tmp/"}"
rm -rf "$TARGET_DIR/$rel"
mkdir -p "$TARGET_DIR/$(dirname "$rel")"
cp -R "$match" "$TARGET_DIR/$rel"
done
shopt -u nullglob
done
rm -rf "$preserve_tmp"
fi