diff --git a/entrypoint-user.sh b/entrypoint-user.sh index fa72e84..b2a1808 100755 --- a/entrypoint-user.sh +++ b/entrypoint-user.sh @@ -86,9 +86,35 @@ if command -v pi &>/dev/null; then # Bootstrap settings.json from template if absent (pi rewrites this # file at runtime — lastChangelogVersion, etc — so we can't symlink it). - if [ ! -f "$HOME/.pi/agent/settings.json" ] && \ - [ -f /opt/pi-toolkit/settings.example.json ]; then - cp /opt/pi-toolkit/settings.example.json "$HOME/.pi/agent/settings.json" + _pi_settings="$HOME/.pi/agent/settings.json" + _pi_template=/opt/pi-toolkit/settings.example.json + if [ ! -f "$_pi_settings" ] && [ -f "$_pi_template" ]; then + cp "$_pi_template" "$_pi_settings" + echo "pi settings.json bootstrapped from template" + elif [ -f "$_pi_settings" ] && [ -f "$_pi_template" ] && \ + [ "${PI_SETTINGS_MERGE:-1}" != "0" ] && command -v jq >/dev/null 2>&1; then + # Non-destructive merge: a settings.json on a PRESERVED volume never + # otherwise sees new template keys (the bootstrap above only fires when + # the file is absent), so config added in an image upgrade — e.g. the + # observational-memory / pi-fork blocks or a newly-enabled model — never + # reaches existing users. Deep-merge with the template FIRST and the + # live file SECOND ('.[0] * .[1]') so the user's values always win and + # only keys MISSING from the live file are filled in from the template. + # Arrays are treated as leaves (the user's array is kept verbatim, so a + # model they deliberately removed is not re-added). Only rewrite when the + # merge actually changes something, and back up the original first. + # Set PI_SETTINGS_MERGE=0 to disable. Invalid JSON on either side → skip, + # never clobber. + if _pi_merged=$(jq -s '.[0] * .[1]' "$_pi_template" "$_pi_settings" 2>/dev/null); then + if [ -n "$_pi_merged" ] && \ + ! printf '%s' "$_pi_merged" | jq -e --slurpfile cur "$_pi_settings" '. == $cur[0]' >/dev/null 2>&1; then + cp "$_pi_settings" "${_pi_settings}.bak.$(date +%Y%m%d-%H%M%S)" + printf '%s\n' "$_pi_merged" > "$_pi_settings" + echo "pi settings.json: merged new template keys from settings.example.json (backup saved)" + fi + else + echo "WARN: pi settings.json merge skipped (jq could not parse template or live file; left untouched)" + fi fi # pi↔mempalace MCP bridge — single extension symlink. diff --git a/scripts/recreate-sanity-check.sh b/scripts/recreate-sanity-check.sh index 41dff1d..57b9dbd 100755 --- a/scripts/recreate-sanity-check.sh +++ b/scripts/recreate-sanity-check.sh @@ -187,6 +187,18 @@ else fail "~/.pi/agent/settings.json missing" fi +# settings.json merge: the entrypoint deep-merges new template keys into a +# preserved settings.json on every start, so config added in an image upgrade +# (e.g. the observational-memory / pi-fork blocks) reaches existing volumes. +# Assert those blocks are present and that the file is still valid JSON. +if command -v jq >/dev/null 2>&1 && [ -f "$HOME/.pi/agent/settings.json" ]; then + if jq -e 'has("observational-memory") and has("pi-fork")' "$HOME/.pi/agent/settings.json" >/dev/null 2>&1; then + pass "settings.json has observational-memory + pi-fork blocks (template merge)" + else + fail "settings.json missing observational-memory and/or pi-fork blocks (template merge did not land)" + fi +fi + # pi package registrations (pi install → recorded in settings.json) if [ -f "$HOME/.pi/agent/settings.json" ]; then for pkg in pi-fork pi-observational-memory; do