feat: scenarios for curator/memo-inbox/pi-grok, deploy and backup tooling
Scenarios - memo-inbox: mirrored by copying; the live directory was not moved or modified and the service was not restarted. All four tracked files match byte for byte (pi-diff.sh reports SAME). Marked deploy = "mirror" so deploy-scenario.sh refuses --apply: applying a mirror would invert the direction of truth and could change a service in daily use. - curator: target configuration, not yet deployed. .pi/SYSTEM.md replaces pi's coding-assistant prompt; durable role text is in .pi/APPEND_SYSTEM.md; profile.toml is the single source of truth for the launch contract. - pi-grok: registered only. It is genuinely a coding agent, so the isolation baseline does not apply in full. Corrections to the documentation, found by testing rather than by reading - AGENTS.override.md does NOT block parent-directory context files; it only shadows its own directory. Verified: with an override file in the workspace, a marker in /tmp/AGENTS.md still reached the system prompt. The only effective switch is --no-context-files, so durable role text must live in .pi/APPEND_SYSTEM.md, which is a system-prompt file and unaffected by -nc. Verified end state: no coding-assistant framing, no pi-docs block, own identity and role text present, no parent pollution, only own skills/tools. - PI_CODING_AGENT_DIR isolates settings/models/auth/trust/extensions/skills/ prompts/themes under the agent directory -- stronger than the --no-* flags because it also repoints credentials -- but does NOT cover ~/.agents/skills. Measured: find-skills, modsearch and summarize still leak. So it complements --no-skills rather than replacing it. - --append-system-prompt accepts a file path, which pi-grok relies on. - cwd is what anchors .pi discovery: a probe that forgot cwd silently lost .pi/SYSTEM.md and kept the coding-assistant persona. Tooling (all dry-run by default; none of them restarts a service) - pi-diff.sh: compares tracked config against the live install in both directions, with a key-redacted comparison for models.json - deploy-scenario.sh: installs a workspace and renders profile.toml into .pi/launch.json, then checks that every referenced path exists - deploy-runtime.sh: renders models.json from its template, refusing placeholder or missing keys. Verified byte-identical to the live file - pi-backup.sh / pi-restore.sh: archives outside the repo, sha256 manifest verified before any restore, live paths preserved rather than overwritten Fixed while testing: pi-backup.sh compared the destination against the repo root literally, so a relative --dest ./backups wrote credential archives into the work tree. Now canonicalised with realpath; ./backups, an absolute in-repo path and ./docs/../backups are all refused.
This commit is contained in:
Executable
+120
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env bash
|
||||
# ---------------------------------------------------------------------------
|
||||
# Compare tracked configuration against the live installation.
|
||||
#
|
||||
# Run this before every deploy, and after every migration, to prove that the
|
||||
# repository and the host agree.
|
||||
#
|
||||
# Usage:
|
||||
# scripts/pi-diff.sh # runtime + every scenario
|
||||
# scripts/pi-diff.sh runtime
|
||||
# scripts/pi-diff.sh <scenario>
|
||||
#
|
||||
# Exit status: 0 when everything matches, 1 when anything differs.
|
||||
# ---------------------------------------------------------------------------
|
||||
# shellcheck source=lib/common.sh
|
||||
. "$(dirname "${BASH_SOURCE[0]}")/lib/common.sh"
|
||||
|
||||
DIFFS=0
|
||||
|
||||
diff_runtime() {
|
||||
head1 "runtime (~/.pi/agent)"
|
||||
local live="$HOME/.pi/agent"
|
||||
local tracked="$REPO_ROOT/runtime/agent"
|
||||
|
||||
diff_file "$tracked/settings.json" "$live/settings.json" "settings.json" || DIFFS=$((DIFFS + 1))
|
||||
|
||||
# models.json is rendered from a template, so compare the redacted forms.
|
||||
if [ -f "$live/models.json" ]; then
|
||||
local a b
|
||||
a="$(mktemp)"; b="$(mktemp)"
|
||||
python3 - "$tracked/models.json.template" >"$a" <<'PY'
|
||||
import json, re, sys
|
||||
print(re.sub(r'"apiKey":\s*".*?"', '"apiKey": "<X>"',
|
||||
json.dumps(json.load(open(sys.argv[1])), ensure_ascii=False, indent=2, sort_keys=True)))
|
||||
PY
|
||||
python3 - "$live/models.json" >"$b" <<'PY'
|
||||
import json, re, sys
|
||||
print(re.sub(r'"apiKey":\s*".*?"', '"apiKey": "<X>"',
|
||||
json.dumps(json.load(open(sys.argv[1])), ensure_ascii=False, indent=2, sort_keys=True)))
|
||||
PY
|
||||
if cmp -s "$a" "$b"; then
|
||||
ok "models.json (key-redacted comparison)"
|
||||
else
|
||||
printf '%s models.json (key-redacted comparison)\n' "${C_YELLOW}DIFF${C_OFF}"
|
||||
diff -u --label repo/models.json.template "$a" --label live/models.json "$b" | sed 's/^/ /'
|
||||
DIFFS=$((DIFFS + 1))
|
||||
fi
|
||||
rm -f "$a" "$b"
|
||||
else
|
||||
warn "live ~/.pi/agent/models.json is absent"
|
||||
DIFFS=$((DIFFS + 1))
|
||||
fi
|
||||
|
||||
local rel
|
||||
while IFS= read -r rel; do
|
||||
diff_file "$tracked/$rel" "$live/$rel" "$rel" || DIFFS=$((DIFFS + 1))
|
||||
done < <(cd "$tracked" && find extensions prompts -type f 2>/dev/null | sort)
|
||||
}
|
||||
|
||||
diff_scenario() {
|
||||
local name="$1" dir
|
||||
dir="$(require_scenario "$name")"
|
||||
local profile="$dir/profile.toml"
|
||||
[ -f "$profile" ] || die "scenario '$name' has no profile.toml"
|
||||
|
||||
local workspace
|
||||
workspace="$(toml_get "$profile" scenario workspace)"
|
||||
[ -n "$workspace" ] || die "scenario '$name': [scenario].workspace is not set"
|
||||
|
||||
head1 "$name ($workspace)"
|
||||
|
||||
if [ ! -d "$dir/workspace" ]; then
|
||||
warn "no tracked workspace/ for '$name'; nothing to compare"
|
||||
return
|
||||
fi
|
||||
if [ ! -d "$workspace" ]; then
|
||||
warn "live workspace $workspace does not exist yet"
|
||||
DIFFS=$((DIFFS + 1))
|
||||
return
|
||||
fi
|
||||
|
||||
local rel
|
||||
while IFS= read -r rel; do
|
||||
diff_file "$dir/workspace/$rel" "$workspace/$rel" "$rel" || DIFFS=$((DIFFS + 1))
|
||||
done < <(cd "$dir/workspace" && find . -type f -printf '%P\n' | sort)
|
||||
|
||||
# Report live files that the repository does not track, so drift is visible in
|
||||
# both directions. Runtime state and toolchains are expected and are skipped.
|
||||
local untracked=()
|
||||
while IFS= read -r rel; do
|
||||
case "$rel" in
|
||||
.ccgram-uploads/*|*/__pycache__/*|*/.venv/*|.venv/*|*.pyc|*.bak-*|\
|
||||
telegram-gateway/*|download.html|.pi/launch.json|*.log) continue ;;
|
||||
esac
|
||||
[ -f "$dir/workspace/$rel" ] || untracked+=("$rel")
|
||||
done < <(cd "$workspace" && find . -type f -printf '%P\n' | sort)
|
||||
|
||||
if [ "${#untracked[@]}" -gt 0 ]; then
|
||||
printf '%sUNTRACKED in live workspace:%s\n' "$C_DIM" "$C_OFF"
|
||||
printf ' %s\n' "${untracked[@]}"
|
||||
fi
|
||||
}
|
||||
|
||||
TARGET="${1:-}"
|
||||
if [ -z "$TARGET" ]; then
|
||||
diff_runtime
|
||||
while IFS= read -r s; do diff_scenario "$s"; done < <(list_scenarios)
|
||||
elif [ "$TARGET" = "runtime" ]; then
|
||||
diff_runtime
|
||||
else
|
||||
diff_scenario "$TARGET"
|
||||
fi
|
||||
|
||||
printf '\n'
|
||||
if [ "$DIFFS" -eq 0 ]; then
|
||||
ok "repository and live installation agree"
|
||||
else
|
||||
warn "$DIFFS difference(s) found"
|
||||
fi
|
||||
exit $((DIFFS > 0))
|
||||
Reference in New Issue
Block a user