pantheon-opencode 1.4.3 → 1.5.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/.pantheon/code-mode/README.md +93 -0
  2. package/.pantheon/code-mode/checkpoint-session.py +7 -0
  3. package/.pantheon/code-mode/checkpoint-session.sh +11 -0
  4. package/.pantheon/code-mode/checkpoint_session.py +481 -0
  5. package/.pantheon/code-mode/compress-inline.py +450 -0
  6. package/.pantheon/code-mode/eval-llm-judge.py +255 -0
  7. package/.pantheon/code-mode/eval-monte-carlo.py +368 -0
  8. package/.pantheon/code-mode/eval-run.py +197 -0
  9. package/.pantheon/code-mode/eval-static.py +321 -0
  10. package/.pantheon/code-mode/example-sync.sh +5 -0
  11. package/.pantheon/code-mode/session-end-save.py +186 -0
  12. package/.pantheon/code-mode/session-end-save.sh +18 -0
  13. package/.pantheon/code-mode/test-allowed.sh +5 -0
  14. package/.pantheon/code-mode/test-auto-continue-checkpoint.sh +179 -0
  15. package/.pantheon/code-mode/test-env.sh +7 -0
  16. package/.pantheon/code-mode/test-prlimit.sh +9 -0
  17. package/.pantheon/code-mode/test-timeout.sh +6 -0
  18. package/README.md +161 -1101
  19. package/README.pt-BR.md +222 -0
  20. package/bin/pantheon-init.mjs +30 -11
  21. package/opencode.json +1 -0
  22. package/package.json +14 -3
  23. package/plugin.json +15 -0
  24. package/scripts/_pantheon_paths.py +48 -7
  25. package/scripts/ci-validate-yaml.py +7 -2
  26. package/scripts/code_mode_server.py +33 -2
  27. package/scripts/cost.mjs +30 -14
  28. package/scripts/doctor.mjs +135 -127
  29. package/scripts/generate-preset-docs.mjs +127 -66
  30. package/scripts/generate-prompts.sh +2 -2
  31. package/scripts/install/cli-ui.mjs +8 -1
  32. package/scripts/install/config-migration.mjs +401 -0
  33. package/scripts/install/health-check.mjs +3 -3
  34. package/scripts/install/model-picker.mjs +72 -23
  35. package/scripts/install/opencode-version.mjs +47 -0
  36. package/scripts/install/opencode.mjs +261 -88
  37. package/scripts/install/plugin.mjs +112 -42
  38. package/scripts/install/shared.mjs +13 -158
  39. package/scripts/install-mcp.mjs +44 -232
  40. package/scripts/mcp_persistence_server.py +382 -16
  41. package/scripts/mcp_resources_server.py +144 -40
  42. package/scripts/memory_mcp_server.py +2 -2
  43. package/scripts/probe-context-rehydrate.mjs +240 -0
  44. package/scripts/probe-pantheon-cost.mjs +304 -0
  45. package/scripts/recover-zenodo-deposition.mjs +146 -0
  46. package/scripts/sync-tui.mjs +32 -7
  47. package/scripts/test-opencode-v1-v2-sandbox.sh +944 -0
  48. package/scripts/uninstall.mjs +298 -954
  49. package/scripts/validate-zenodo-release.mjs +154 -0
  50. package/scripts/zenodo-artifact.mjs +21 -0
  51. package/scripts/zenodo-release-state.mjs +53 -0
  52. package/src/agents/zeus.md +25 -14
  53. package/src/mcp/_pantheon_paths.py +48 -7
  54. package/src/mcp/code_mode_server.py +33 -2
  55. package/src/mcp/eval_store.py +1 -1
  56. package/src/mcp/mcp_persistence_server.py +382 -16
  57. package/src/mcp/mcp_resources_server.py +144 -40
  58. package/src/mcp/pantheon_vision_server.py +1 -1
  59. package/src/mcp/toon_codec.py +491 -0
  60. package/src/pantheon/background-job-board.ts +67 -1
  61. package/src/pantheon/cost-command.ts +60 -39
  62. package/src/pantheon/delegate-manager.ts +669 -0
  63. package/src/pantheon/delegation-classifier.ts +307 -0
  64. package/src/pantheon/delegation-enforce.ts +13 -12
  65. package/src/pantheon/delegation-finalize.ts +374 -0
  66. package/src/pantheon/delegation-notify.ts +212 -0
  67. package/src/pantheon/delegation-toast.ts +73 -0
  68. package/src/pantheon/delegation.ts +1120 -0
  69. package/src/pantheon/goal-loop.ts +2 -2
  70. package/src/pantheon/goal-store.ts +1 -1
  71. package/src/pantheon/hashline/tool.ts +2 -2
  72. package/src/pantheon/model-command.ts +2 -1
  73. package/src/pantheon/permission-eval.ts +284 -0
  74. package/src/pantheon/step-cap.ts +110 -0
  75. package/src/pantheon/token-opt.ts +781 -0
  76. package/src/pantheon/toon-codec.ts +481 -0
  77. package/src/pantheon/tui-version-check.ts +1 -3
  78. package/src/pantheon/v2-bridge.ts +129 -0
  79. package/src/pantheon/v2-events.ts +131 -0
  80. package/src/pantheon/v2-hooks.ts +287 -0
  81. package/src/pantheon/v2-tool-definitions.ts +331 -0
  82. package/src/pantheon/zeus-delegate-with-retry.ts +406 -0
  83. package/src/plugin-v2/index.ts +21 -0
  84. package/src/plugin-v2.ts +824 -0
  85. package/src/plugin.ts +236 -42
  86. package/src/plugins/tui/dist/server.d.ts +1 -1
  87. package/src/plugins/tui/dist/server.d.ts.map +1 -1
  88. package/src/plugins/tui/dist/server.js +1 -1
  89. package/src/plugins/tui/dist/server.js.map +1 -1
  90. package/src/plugins/tui/dist/tui.d.ts +10 -7
  91. package/src/plugins/tui/dist/tui.d.ts.map +1 -1
  92. package/src/plugins/tui/dist/tui.js +11 -8
  93. package/src/plugins/tui/dist/tui.js.map +1 -1
  94. package/src/plugins/tui/dist/tui.tsx +22 -16
  95. package/src/plugins/tui/package-lock.json +51 -51
  96. package/src/plugins/tui/package.json +7 -7
  97. package/src/plugins/tui/src/index.tsx +28 -24
  98. package/src/skills/README.md +0 -2
  99. package/src/pantheon/tool-context.ts +0 -41
@@ -0,0 +1,944 @@
1
+ #!/usr/bin/env bash
2
+ # test-opencode-v1-v2-sandbox.sh — Pantheon global-install sandbox validator
3
+ # for OpenCode V1 (`opencode`) and V2 (`opencode2`) side-by-side.
4
+ #
5
+ # Validates the installed pantheon-opencode package AS A REAL USER inside an
6
+ # isolated sandbox (own HOME, npm prefix and venv), never the dev environment.
7
+ #
8
+ # Modes (combinable):
9
+ # --prepare Build tarball, install pantheon-opencode + OpenCode V1/V2
10
+ # binaries in the sandbox prefix, headless init, and generate
11
+ # the per-version project configs and sandbox run-test.sh.
12
+ # --run v1|v2 Base validation for one version: opencode mcp list + doctor
13
+ # (config is regenerated for that version first).
14
+ # --prompts Prompt battery via `opencode run --format json` for every
15
+ # requested version (with --run vX) or both versions alone.
16
+ # --cost Offline/ambiental pantheon_cost probe for the requested
17
+ # version (with --run vX) or both versions alone.
18
+ # The probe's synthetic checks run in explicit --fixture mode.
19
+ # --rehydrate Offline/ambiental context_rehydrate + context_session_summary
20
+ # probe for the requested version (with --run vX) or both.
21
+ # --reset Wipe the sandbox root.
22
+ # --help Usage.
23
+ #
24
+ # Env overrides:
25
+ # PANTHEON_SANDBOX_ROOT Sandbox root (default: ~/pantheon-sandbox)
26
+ # OPENCODE_V1_SPEC npm spec providing the `opencode` binary
27
+ # (default: opencode-ai@1.18.18)
28
+ # OPENCODE_V2_SPEC npm spec providing the `opencode2` binary
29
+ # (default: @opencode-ai/cli@beta)
30
+ # PANTHEON_SANDBOX_MODEL Model used by init/prompts
31
+ # (default: opencode-go/mimo-v2.5)
32
+ # PANTHEON_PROMPT_TIMEOUT Per-prompt timeout in seconds (default: 300)
33
+ # PANTHEON_RETRY_COOLDOWN Cooldown seconds before the single FAIL retry
34
+ # (default: 60; retry only for real FAIL, never AMBIENTAL)
35
+ #
36
+ # Exit codes: 0 = no real failures (AMBIENTAL issues do not fail the run),
37
+ # 1 = real FAIL (see prompts-report.md in the sandbox root),
38
+ # 2 = usage error, 3 = sandbox not prepared.
39
+ set -euo pipefail
40
+
41
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
42
+ REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
43
+
44
+ SANDBOX_ROOT="${PANTHEON_SANDBOX_ROOT:-$HOME/pantheon-sandbox}"
45
+ SANDBOX_HOME="$SANDBOX_ROOT/home"
46
+ NPM_PREFIX="$SANDBOX_HOME/.npm-global"
47
+
48
+ OPENCODE_V1_SPEC="${OPENCODE_V1_SPEC:-opencode-ai@1.18.18}"
49
+ OPENCODE_V2_SPEC="${OPENCODE_V2_SPEC:-@opencode-ai/cli@beta}"
50
+ PANTHEON_SANDBOX_MODEL="${PANTHEON_SANDBOX_MODEL:-opencode-go/mimo-v2.5}"
51
+ PANTHEON_PROMPT_TIMEOUT="${PANTHEON_PROMPT_TIMEOUT:-300}"
52
+
53
+ V1_BIN="${OPENCODE_V1_BIN:-opencode}"
54
+ V2_BIN="${OPENCODE_V2_BIN:-opencode2}"
55
+
56
+ REPORT_FILE="$SANDBOX_ROOT/prompts-report.md"
57
+ COST_REPORT_FILE="$SANDBOX_ROOT/pantheon-cost-report.md"
58
+ REHYDRATE_REPORT_FILE="$SANDBOX_ROOT/context-rehydrate-report.md"
59
+ EXTRACT_PY="$SANDBOX_ROOT/.prompt-extract-json.py"
60
+
61
+ MODE_PREPARE=0
62
+ MODE_RESET=0
63
+ MODE_PROMPTS=0
64
+ MODE_COST=0
65
+ MODE_REHYDRATE=0
66
+ RUN_VERSION=""
67
+
68
+ usage() {
69
+ sed -n '2,40p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
70
+ exit 2
71
+ }
72
+
73
+ log() { printf '%s\n' "$*"; }
74
+ warn() { printf 'WARN: %s\n' "$*" >&2; }
75
+ die() { printf 'ERROR: %s\n' "$*" >&2; exit 1; }
76
+
77
+ bin_for() {
78
+ case "$1" in
79
+ v1) printf '%s' "$V1_BIN" ;;
80
+ v2) printf '%s' "$V2_BIN" ;;
81
+ *) return 1 ;;
82
+ esac
83
+ }
84
+
85
+ project_for() { printf '%s/project-%s' "$SANDBOX_ROOT" "$1"; }
86
+
87
+ # Resolve a binary STRICTLY inside the sandbox npm prefix — never fall back to
88
+ # the dev PATH, otherwise a non-prepared sandbox would silently test the host
89
+ # installation.
90
+ sandbox_bin_for() { # name → prints path; rc 1 if not inside sandbox prefix
91
+ local p
92
+ p="$(command -v "$1" 2>/dev/null || true)"
93
+ case "$p" in
94
+ "$NPM_PREFIX"/*) printf '%s' "$p" ;;
95
+ *) return 1 ;;
96
+ esac
97
+ }
98
+
99
+ sandbox_bin_v() { # v → echoes path; rc 1 if missing
100
+ sandbox_bin_for "$(bin_for "$1")"
101
+ }
102
+
103
+ # ── Sandbox environment isolation ─────────────────────────────────────────────
104
+
105
+ sandbox_env() {
106
+ export HOME="$SANDBOX_HOME"
107
+ export PATH="$NPM_PREFIX/bin:$HOME/.config/opencode/.venv/bin:$PATH"
108
+ export npm_config_prefix="$NPM_PREFIX"
109
+ }
110
+
111
+ # ── Gate (b): sandbox run-test.sh with pantheon://agents content validation ──
112
+
113
+ write_run_test_sh() {
114
+ cat > "$SANDBOX_ROOT/run-test.sh" <<'RUNTEST'
115
+ #!/usr/bin/env bash
116
+ # Generated by pantheon-opencode scripts/test-opencode-v1-v2-sandbox.sh --prepare
117
+ # Validates the global install: binaries, exactly 5 connected MCPs per version,
118
+ # doctor 0 errors, AND the CONTENT of pantheon://agents (zeus/hermes present).
119
+ set -euo pipefail
120
+
121
+ SANDBOX_DIR="$(cd "$(dirname "$0")" && pwd)"
122
+ SANDBOX_HOME="$SANDBOX_DIR/home"
123
+ NPM_PREFIX="$SANDBOX_HOME/.npm-global"
124
+ PANTHEON_GLOBAL="$SANDBOX_HOME/.config/opencode"
125
+ SANDBOX_VENV="$PANTHEON_GLOBAL/.venv"
126
+ REPO_DIR="${PANTHEON_REPO:-$(cd "$SANDBOX_DIR/../pantheon" && pwd)}"
127
+
128
+ echo "=== Pantheon Sandbox Test ==="
129
+ echo "Sandbox: $SANDBOX_DIR"
130
+ echo "Repo: $REPO_DIR"
131
+
132
+ export HOME="$SANDBOX_HOME"
133
+ export PATH="$NPM_PREFIX/bin:$SANDBOX_VENV/bin:$PATH"
134
+ export npm_config_prefix="$NPM_PREFIX"
135
+
136
+ STATUS_FAIL=0
137
+ STATUS_AMBIENTAL=0
138
+ STATUS_NOT_TESTED=0
139
+ STATUS_PASS=0
140
+ MCP_AMBIENTAL_RE='needs authentication|unauthorized|authentication failure|api key|credentials|401|docker|econnrefused|enotfound|etimedout|econnreset|connection refused|rate.?limit|429|quota|overloaded|edgesout|npm err'
141
+
142
+ record_status() { # label status detail
143
+ local label="$1" status="$2" detail="$3"
144
+ case "$status" in
145
+ PASS) STATUS_PASS=$((STATUS_PASS + 1)) ;;
146
+ FAIL) STATUS_FAIL=$((STATUS_FAIL + 1)) ;;
147
+ AMBIENTAL) STATUS_AMBIENTAL=$((STATUS_AMBIENTAL + 1)) ;;
148
+ NOT_TESTED) STATUS_NOT_TESTED=$((STATUS_NOT_TESTED + 1)) ;;
149
+ esac
150
+ printf '%s: %s — %s\n' "$status" "$label" "$detail"
151
+ }
152
+
153
+ check_binary() { # label binary
154
+ local label="$1" bin="$2" out rc=0
155
+ if ! command -v "$bin" >/dev/null 2>&1; then
156
+ record_status "$label" "NOT_TESTED" "binary '$bin' is not installed in the sandbox prefix"
157
+ return 0
158
+ fi
159
+ set +e
160
+ out="$("$bin" --version 2>&1)"
161
+ rc=$?
162
+ set -e
163
+ if [ "$rc" -eq 0 ]; then
164
+ record_status "$label" "PASS" "${out:-version command exited 0}"
165
+ elif printf '%s' "$out" | grep -Eiq "$MCP_AMBIENTAL_RE"; then
166
+ record_status "$label" "AMBIENTAL" "version check unavailable (exit $rc)"
167
+ else
168
+ record_status "$label" "FAIL" "version check exited $rc"
169
+ fi
170
+ }
171
+
172
+ check_mcp() { # label binary project
173
+ local label="$1" bin="$2" project="$3" out connected_count rc=0
174
+ if ! command -v "$bin" >/dev/null 2>&1; then
175
+ record_status "$label MCP" "NOT_TESTED" "binary '$bin' is not installed in the sandbox prefix"
176
+ return 0
177
+ fi
178
+ if [ ! -d "$project" ]; then
179
+ record_status "$label MCP" "NOT_TESTED" "project directory is missing: $project"
180
+ return 0
181
+ fi
182
+ set +e
183
+ out="$(cd "$project" && "$bin" mcp list 2>&1)"
184
+ rc=$?
185
+ set -e
186
+ out="$(printf '%s' "$out" | sed -E $'s/\x1B\\[[0-?]*[ -/]*[@-~]//g')"
187
+ printf '%s\n' "$out"
188
+ if [ "$rc" -ne 0 ]; then
189
+ if printf '%s' "$out" | grep -Eiq "$MCP_AMBIENTAL_RE"; then
190
+ record_status "$label MCP" "AMBIENTAL" "mcp list environmental failure (exit $rc)"
191
+ else
192
+ record_status "$label MCP" "FAIL" "mcp list exited $rc"
193
+ fi
194
+ return 0
195
+ fi
196
+ connected_count="$(printf '%s' "$out" | grep -Eic '(^|[^[:alnum:]_])connected([^[:alnum:]_]|$)')" || connected_count=0
197
+ if [ "$connected_count" -eq 5 ] \
198
+ && ! printf '%s' "$out" | grep -Eiq 'failed|✘' \
199
+ && ! printf '%s' "$out" | grep -Eiq "$MCP_AMBIENTAL_RE"; then
200
+ record_status "$label MCP" "PASS" "exactly 5 connected MCPs"
201
+ elif printf '%s' "$out" | grep -Eiq "$MCP_AMBIENTAL_RE"; then
202
+ record_status "$label MCP" "AMBIENTAL" "environmental MCP status; found $connected_count connected"
203
+ else
204
+ record_status "$label MCP" "FAIL" "expected exactly 5 connected MCPs; found $connected_count or failures reported"
205
+ fi
206
+ }
207
+
208
+ echo "--- Binaries ---"
209
+ check_binary "V1 binary" opencode
210
+ check_binary "V2 binary" opencode2
211
+
212
+ echo "--- Building tarball ---"
213
+ cd "$REPO_DIR"
214
+ npm pack 2>/dev/null || { echo "ERROR: npm pack failed"; exit 1; }
215
+ TGZ=$(ls -t pantheon-opencode-*.tgz | head -1)
216
+ npm rm -g pantheon-opencode 2>/dev/null || true
217
+ npm install -g "$TGZ"
218
+ if ! git -C "$REPO_DIR" ls-files --error-unmatch -- "$TGZ" >/dev/null 2>&1; then rm -f -- "$TGZ"; fi
219
+
220
+ echo "--- Init (headless) ---"
221
+ pantheon-opencode init --headless -y
222
+
223
+ echo "--- MCP Validation ---"
224
+ check_mcp "V1" opencode "$SANDBOX_DIR/project-v1"
225
+ check_mcp "V2" opencode2 "$SANDBOX_DIR/project-v2"
226
+
227
+ echo "--- Doctor ---"
228
+ if command -v pantheon-opencode >/dev/null 2>&1; then
229
+ set +e
230
+ DOCTOR_OUTPUT="$(pantheon-opencode doctor --target "$SANDBOX_DIR" 2>&1)"
231
+ DOCTOR_RC=$?
232
+ set -e
233
+ if [ "$DOCTOR_RC" -eq 0 ]; then
234
+ record_status "doctor" "PASS" "exit 0"
235
+ elif printf '%s' "$DOCTOR_OUTPUT" | grep -Eiq "$MCP_AMBIENTAL_RE"; then
236
+ record_status "doctor" "AMBIENTAL" "environmental doctor failure (exit $DOCTOR_RC)"
237
+ else
238
+ record_status "doctor" "FAIL" "doctor exited $DOCTOR_RC"
239
+ fi
240
+ else
241
+ record_status "doctor" "NOT_TESTED" "pantheon-opencode is not installed in the sandbox prefix"
242
+ fi
243
+
244
+ echo "--- Gate (b): pantheon://agents CONTENT check ---"
245
+ RESOURCES_SRV="$PANTHEON_GLOBAL/scripts/mcp_resources_server.py"
246
+ if [ ! -f "$RESOURCES_SRV" ]; then
247
+ record_status "pantheon://agents resource" "NOT_TESTED" "resources server not found at $RESOURCES_SRV"
248
+ else
249
+ set +e
250
+ AGENTS_CONTENT="$("$SANDBOX_VENV/bin/python" - "$RESOURCES_SRV" <<'PYEOF'
251
+ import asyncio, importlib.util, sys
252
+ from pathlib import Path
253
+ resource_path = Path(sys.argv[1]).resolve()
254
+ sys.path.insert(0, str(resource_path.parent))
255
+ spec = importlib.util.spec_from_file_location("mrs", sys.argv[1])
256
+ mod = importlib.util.module_from_spec(spec)
257
+ spec.loader.exec_module(mod)
258
+ print(asyncio.run(mod.list_agents()))
259
+ PYEOF
260
+ )"
261
+ AGENTS_RC=$?
262
+ set -e
263
+ printf '%s\n' "$AGENTS_CONTENT" | head -8
264
+ if [ "$AGENTS_RC" -ne 0 ]; then
265
+ if printf '%s' "$AGENTS_CONTENT" | grep -Eiq "$MCP_AMBIENTAL_RE"; then
266
+ record_status "pantheon://agents resource" "AMBIENTAL" "resource probe environmental failure (exit $AGENTS_RC)"
267
+ else
268
+ record_status "pantheon://agents resource" "FAIL" "resource probe exited $AGENTS_RC"
269
+ fi
270
+ elif printf '%s\n' "$AGENTS_CONTENT" | grep -qi 'zeus' \
271
+ && printf '%s\n' "$AGENTS_CONTENT" | grep -qi 'hermes'; then
272
+ record_status "pantheon://agents resource" "PASS" "pantheon://agents content includes zeus and hermes"
273
+ else
274
+ record_status "pantheon://agents resource" "FAIL" "content missing zeus/hermes (connectivity alone is not enough)"
275
+ fi
276
+ fi
277
+ echo ""
278
+ echo "=== VALIDATION COMPLETE ==="
279
+ if [ "$STATUS_FAIL" -gt 0 ]; then
280
+ FINAL_VERDICT="FAIL"
281
+ elif [ "$STATUS_NOT_TESTED" -gt 0 ]; then
282
+ FINAL_VERDICT="NOT_TESTED"
283
+ elif [ "$STATUS_AMBIENTAL" -gt 0 ]; then
284
+ FINAL_VERDICT="AMBIENTAL"
285
+ else
286
+ FINAL_VERDICT="PASS"
287
+ fi
288
+ printf 'Final verdict: %s\n' "$FINAL_VERDICT"
289
+ if [ "$FINAL_VERDICT" = "FAIL" ]; then
290
+ exit 1
291
+ fi
292
+ echo "Next: open the isolated TUI with: $SANDBOX_DIR/start-pantheon.sh"
293
+ RUNTEST
294
+ chmod +x "$SANDBOX_ROOT/run-test.sh"
295
+ }
296
+
297
+ # ── Reset / Prepare ───────────────────────────────────────────────────────────
298
+
299
+ cmd_reset() {
300
+ case "$SANDBOX_ROOT" in
301
+ "/" | "$HOME" | "$REPO_DIR" | "$REPO_DIR"/*)
302
+ die "refusing to reset unsafe sandbox root: $SANDBOX_ROOT" ;;
303
+ esac
304
+ log "Resetting sandbox: $SANDBOX_ROOT"
305
+ rm -rf "$SANDBOX_ROOT"
306
+ log "Sandbox removed."
307
+ }
308
+
309
+ install_binaries() {
310
+ log "--- Installing pantheon-opencode (from repo tarball) ---"
311
+ cd "$REPO_DIR"
312
+ npm pack 2>/dev/null || die "npm pack failed"
313
+ local tgz
314
+ tgz=$(ls -t pantheon-opencode-*.tgz | head -1)
315
+ npm rm -g pantheon-opencode 2>/dev/null || true
316
+ npm install -g "$REPO_DIR/$tgz" || die "install of $tgz failed"
317
+ if ! git -C "$REPO_DIR" ls-files --error-unmatch -- "$tgz" >/dev/null 2>&1; then rm -f -- "$tgz"; fi
318
+ log "--- Installing OpenCode V1 ($OPENCODE_V1_SPEC) ---"
319
+ npm install -g "$OPENCODE_V1_SPEC" \
320
+ || warn "V1 binary install failed ($OPENCODE_V1_SPEC) — v1 prompts will be AMBIENTAL"
321
+ log "--- Installing OpenCode V2 ($OPENCODE_V2_SPEC) ---"
322
+ npm install -g "$OPENCODE_V2_SPEC" \
323
+ || warn "V2 binary install failed ($OPENCODE_V2_SPEC) — v2 prompts will be AMBIENTAL"
324
+ }
325
+
326
+ prepare_project() {
327
+ local v="$1" dir pan
328
+ dir="$(project_for "$v")"
329
+ mkdir -p "$dir"
330
+ pan="$(sandbox_bin_for "pantheon-opencode")" \
331
+ || die "pantheon-opencode not installed in sandbox prefix — run $0 --prepare first"
332
+ log "--- Regenerating config for $v in $dir ---"
333
+ (cd "$dir" && "$pan" init --project --version "$v" --model "$PANTHEON_SANDBOX_MODEL" -y) \
334
+ || die "init --project --version $v failed in $dir"
335
+ }
336
+
337
+ cmd_prepare() {
338
+ mkdir -p "$SANDBOX_HOME" "$NPM_PREFIX"
339
+ write_run_test_sh
340
+ sandbox_env
341
+ install_binaries
342
+ local pan
343
+ pan="$(sandbox_bin_for "pantheon-opencode")" \
344
+ || die "pantheon-opencode not found in sandbox prefix after install"
345
+ log "--- Headless init (global venv + runtime) ---"
346
+ "$pan" init --headless -y --model "$PANTHEON_SANDBOX_MODEL" \
347
+ || die "pantheon-opencode init --headless failed"
348
+ prepare_project v1
349
+ prepare_project v2
350
+ write_extract_py
351
+ log "Prepare complete. Sandbox: $SANDBOX_ROOT"
352
+ log "Next: $0 --run v1 --prompts | $0 --run v2 --prompts"
353
+ }
354
+
355
+ # ── JSON text extraction (schema-agnostic) ────────────────────────────────────
356
+
357
+ write_extract_py() {
358
+ cat > "$EXTRACT_PY" <<'PYEOF'
359
+ """Collect every string value from opencode run --format json output."""
360
+ import json
361
+ import sys
362
+
363
+
364
+ def walk(obj, out):
365
+ if isinstance(obj, str):
366
+ out.append(obj)
367
+ elif isinstance(obj, dict):
368
+ for value in obj.values():
369
+ walk(value, out)
370
+ elif isinstance(obj, list):
371
+ for value in obj:
372
+ walk(value, out)
373
+
374
+
375
+ raw = sys.stdin.read()
376
+ chunks = []
377
+ try:
378
+ walk(json.loads(raw), chunks)
379
+ except Exception:
380
+ for line in raw.splitlines():
381
+ line = line.strip()
382
+ if not line:
383
+ continue
384
+ try:
385
+ walk(json.loads(line), chunks)
386
+ except Exception:
387
+ chunks.append(line)
388
+ print("\n".join(chunks))
389
+ PYEOF
390
+ }
391
+
392
+ # ── Prompt battery ────────────────────────────────────────────────────────────
393
+
394
+ # Known environmental failure signatures (never fail the run).
395
+ AMBIENTAL_RE='bifrost|unauthorized|authentication failure|api key|credentials|401|docker|econnrefused|enotfound|etimedout|econnreset|connection refused|rate.?limit|429|quota|overloaded|edgesout|npm err'
396
+ MCP_AMBIENTAL_RE='needs authentication|unauthorized|authentication failure|api key|credentials|401|docker|econnrefused|enotfound|etimedout|econnreset|connection refused|rate.?limit|429|quota|overloaded|edgesout|npm err'
397
+
398
+ PROMPT_IDS=()
399
+ PROMPT_TEXTS=()
400
+ PROMPT_MARKERS=()
401
+ PROMPT_AMB_MARKERS=()
402
+ PROMPT_VERIFY=()
403
+
404
+ add_prompt() { # id marker ambiental_marker verify text
405
+ PROMPT_IDS+=("$1")
406
+ PROMPT_MARKERS+=("$2")
407
+ PROMPT_AMB_MARKERS+=("$3")
408
+ PROMPT_VERIFY+=("$4")
409
+ PROMPT_TEXTS+=("$5")
410
+ }
411
+
412
+ build_battery() {
413
+ local v="$1"
414
+ PROMPT_IDS=()
415
+ PROMPT_TEXTS=()
416
+ PROMPT_MARKERS=()
417
+ PROMPT_AMB_MARKERS=()
418
+ PROMPT_VERIFY=()
419
+ add_prompt "agents-resource" "AGENTS-OK" "AGENTS-MISSING" "" \
420
+ "List your MCP resources and read the resource pantheon://agents. If the returned agent list includes an agent named 'zeus' and one named 'hermes', reply with exactly: AGENTS-OK. Otherwise reply with exactly: AGENTS-MISSING."
421
+ add_prompt "memory-store" "MEMORY-STORE-OK" "" "" \
422
+ "Use the pantheon-memory memory_store tool to store this exact entry: namespace 'default', key 'pantheon-sandbox-probe-$v', value 'alive'. Then reply with exactly: MEMORY-STORE-OK"
423
+ add_prompt "memory-recall" "MEMORY-RECALL-OK" "" "" \
424
+ "Use the pantheon-memory memory_recall tool (or memory_search) to look up key 'pantheon-sandbox-probe-$v' in namespace 'default'. If the stored value is 'alive', reply with exactly: MEMORY-RECALL-OK"
425
+ add_prompt "tmp-write-read" "TMP-OK" "" \
426
+ "${TMPDIR:-/tmp}/pantheon-sandbox-probe-$v.txt::TMP-OK" \
427
+ "Using the bash tool, run a python3 -c command that writes the text TMP-OK to the file ${TMPDIR:-/tmp}/pantheon-sandbox-probe-$v.txt (overwrite it), then run another python3 -c command to read that file back. Do not use any other tool or command to write or read the file — only python3 -c. If what you read back is exactly TMP-OK, reply with exactly: TMP-OK"
428
+ add_prompt "delegation" "DELEGATION-OK" "DELEGATION-UNAVAILABLE" "" \
429
+ "Delegate a tiny task to the @talos agent via the task tool: ask it to reply with the single word PONG. If the delegated response contains PONG, reply with exactly: DELEGATION-OK. If you cannot delegate to agents in this context, reply with exactly: DELEGATION-UNAVAILABLE"
430
+ }
431
+
432
+ declare -A RESULTS
433
+ declare -A DETAILS
434
+
435
+ record() { # v id result detail
436
+ RESULTS["$1:$2"]="$3"
437
+ DETAILS["$1:$2"]="$4"
438
+ }
439
+
440
+ extract_text() { "$HOME/.config/opencode/.venv/bin/python" "$EXTRACT_PY" < "$1"; }
441
+
442
+ ATTEMPT_RESULT=""
443
+ ATTEMPT_DETAIL=""
444
+
445
+ run_prompt_attempt() { # v idx current_bin — sets ATTEMPT_RESULT / ATTEMPT_DETAIL
446
+ local v="$1" idx="$2" bin="$3" id marker amb_marker verify text
447
+ id="${PROMPT_IDS[$idx]}"
448
+ marker="${PROMPT_MARKERS[$idx]}"
449
+ amb_marker="${PROMPT_AMB_MARKERS[$idx]}"
450
+ verify="${PROMPT_VERIFY[$idx]}"
451
+ text="${PROMPT_TEXTS[$idx]}"
452
+
453
+ # Remove the probe file first so post-run verification reflects THIS run.
454
+ if [ -n "$verify" ]; then
455
+ rm -f "${verify%%::*}"
456
+ fi
457
+
458
+ local out_file err_file rc=0
459
+ out_file="$(mktemp)"
460
+ err_file="$(mktemp)"
461
+
462
+ set +e
463
+ (cd "$(project_for "$v")" \
464
+ && timeout "$PANTHEON_PROMPT_TIMEOUT" "$bin" run --auto --format json "$text") \
465
+ > "$out_file" 2> "$err_file"
466
+ rc=$?
467
+ set -e
468
+
469
+ local extracted
470
+ extracted="$(extract_text "$out_file" || true)"
471
+ local raw
472
+ raw="$( { tr '\n' ' ' < "$out_file"; tr '\n' ' ' < "$err_file"; } | cut -c1-400)"
473
+ rm -f "$out_file" "$err_file"
474
+
475
+ if [ "$rc" -eq 124 ]; then
476
+ ATTEMPT_RESULT="AMBIENTAL"
477
+ ATTEMPT_DETAIL="timeout after ${PANTHEON_PROMPT_TIMEOUT}s (provider latency)"
478
+ elif [ "$rc" -ne 0 ]; then
479
+ if [ -n "$amb_marker" ] && printf '%s' "$extracted" | grep -q "$amb_marker"; then
480
+ ATTEMPT_RESULT="AMBIENTAL"
481
+ ATTEMPT_DETAIL="explicit $amb_marker (exit $rc)"
482
+ elif printf '%s' "$raw" | grep -Eiq "$AMBIENTAL_RE"; then
483
+ ATTEMPT_RESULT="AMBIENTAL"
484
+ ATTEMPT_DETAIL="environmental signature with exit $rc"
485
+ else
486
+ ATTEMPT_RESULT="FAIL"
487
+ ATTEMPT_DETAIL="marker $marker absent; exit $rc; snippet: $(printf '%s' "$raw" | cut -c1-160)"
488
+ fi
489
+ elif printf '%s' "$extracted" | grep -q "$marker"; then
490
+ ATTEMPT_RESULT="PASS"
491
+ ATTEMPT_DETAIL="marker $marker found"
492
+ elif [ -n "$verify" ] \
493
+ && [ "$(cat "${verify%%::*}" 2>/dev/null)" = "${verify##*::}" ]; then
494
+ ATTEMPT_RESULT="PASS"
495
+ ATTEMPT_DETAIL="probe file verified: ${verify%%::*} contains ${verify##*::}"
496
+ elif printf '%s' "$raw" | grep -Eiq "$AMBIENTAL_RE"; then
497
+ local sig
498
+ sig="$(printf '%s' "$raw" | grep -Eio "$AMBIENTAL_RE" | head -1)"
499
+ ATTEMPT_RESULT="AMBIENTAL"
500
+ ATTEMPT_DETAIL="environmental signature: $sig"
501
+ else
502
+ ATTEMPT_RESULT="FAIL"
503
+ ATTEMPT_DETAIL="marker $marker absent; rc=$rc; snippet: $(printf '%s' "$raw" | cut -c1-160)"
504
+ fi
505
+ }
506
+
507
+ RETRY_COOLDOWN="${PANTHEON_RETRY_COOLDOWN:-60}"
508
+ RETRIES_USED=0
509
+
510
+ run_prompt() { # v idx current_bin — ONE retry after cooldown for real FAIL only
511
+ local v="$1" idx="$2" bin="$3" id first_detail
512
+ id="${PROMPT_IDS[$idx]}"
513
+ run_prompt_attempt "$v" "$idx" "$bin"
514
+ if [ "$ATTEMPT_RESULT" = "FAIL" ]; then
515
+ first_detail="$ATTEMPT_DETAIL"
516
+ log "[$v] prompt $id FAIL on first attempt — retrying once after ${RETRY_COOLDOWN}s cooldown"
517
+ sleep "$RETRY_COOLDOWN"
518
+ run_prompt_attempt "$v" "$idx" "$bin"
519
+ RETRIES_USED=$((RETRIES_USED + 1))
520
+ if [ "$ATTEMPT_RESULT" != "FAIL" ]; then
521
+ ATTEMPT_DETAIL="$ATTEMPT_DETAIL [retry used — first attempt: $first_detail]"
522
+ else
523
+ ATTEMPT_DETAIL="$ATTEMPT_DETAIL [FAIL persisted after retry — first attempt: $first_detail]"
524
+ fi
525
+ fi
526
+ record "$v" "$id" "$ATTEMPT_RESULT" "$ATTEMPT_DETAIL"
527
+ }
528
+
529
+ check_mcp_list() { # v
530
+ local v="$1" bin out connected_count rc=0
531
+ if ! bin="$(sandbox_bin_v "$v")"; then
532
+ record "$v" "mcp-list" "AMBIENTAL" "binary not installed in sandbox prefix"
533
+ return 0
534
+ fi
535
+ set +e
536
+ out="$(cd "$(project_for "$v")" && "$bin" mcp list 2>&1)"
537
+ rc=$?
538
+ set -e
539
+ out="$(printf '%s' "$out" | sed -E $'s/\x1B\\[[0-?]*[ -/]*[@-~]//g')"
540
+ if [ "$rc" -ne 0 ]; then
541
+ if printf '%s' "$out" | grep -Eiq "$MCP_AMBIENTAL_RE"; then
542
+ record "$v" "mcp-list" "AMBIENTAL" "mcp list environmental failure (exit $rc)"
543
+ else
544
+ record "$v" "mcp-list" "FAIL" "opencode mcp list exited $rc"
545
+ fi
546
+ return 0
547
+ fi
548
+ connected_count="$(printf '%s' "$out" | grep -Eic '(^|[^[:alnum:]_])connected([^[:alnum:]_]|$)')" || connected_count=0
549
+ if [ "$connected_count" -eq 5 ] \
550
+ && ! printf '%s' "$out" | grep -Eiq "failed|✘" \
551
+ && ! printf '%s' "$out" | grep -Eiq "$MCP_AMBIENTAL_RE"; then
552
+ record "$v" "mcp-list" "PASS" "$connected_count connected (expected exactly 5)"
553
+ elif printf '%s' "$out" | grep -Eiq "$MCP_AMBIENTAL_RE"; then
554
+ record "$v" "mcp-list" "AMBIENTAL" "environmental MCP status; found $connected_count connected"
555
+ else
556
+ record "$v" "mcp-list" "FAIL" "expected exactly 5 connected MCPs; found $connected_count or failures reported"
557
+ fi
558
+ }
559
+
560
+ check_doctor() { # v
561
+ local v="$1" pan rc=0
562
+ if ! pan="$(sandbox_bin_for "pantheon-opencode")"; then
563
+ record "$v" "doctor" "AMBIENTAL" "pantheon-opencode not installed in sandbox prefix"
564
+ return 0
565
+ fi
566
+ set +e
567
+ (cd "$(project_for "$v")" && "$pan" doctor --target "$(project_for "$v")") > /dev/null 2>&1
568
+ rc=$?
569
+ set -e
570
+ if [ "$rc" -eq 0 ]; then
571
+ record "$v" "doctor" "PASS" "exit 0"
572
+ else
573
+ record "$v" "doctor" "FAIL" "doctor exited $rc (blocking errors)"
574
+ fi
575
+ }
576
+
577
+ installed_package_root() {
578
+ local pan resolved
579
+ if ! pan="$(sandbox_bin_for "pantheon-opencode")"; then
580
+ return 1
581
+ fi
582
+ resolved="$(readlink -f "$pan" 2>/dev/null || true)"
583
+ [ -n "$resolved" ] || return 1
584
+ dirname "$(dirname "$resolved")"
585
+ }
586
+
587
+ run_cost_probe() { # v
588
+ local v="$1" package_root probe raw status detail
589
+ if ! package_root="$(installed_package_root)"; then
590
+ record "$v" "pantheon-cost" "NOT_TESTED" "sandbox package is not installed"
591
+ return 0
592
+ fi
593
+ probe="$package_root/scripts/probe-pantheon-cost.mjs"
594
+ if [ ! -f "$probe" ]; then
595
+ record "$v" "pantheon-cost" "NOT_TESTED" "installed package has no offline cost probe"
596
+ return 0
597
+ fi
598
+ if ! command -v node >/dev/null 2>&1; then
599
+ record "$v" "pantheon-cost" "AMBIENTAL" "node runtime is not available"
600
+ return 0
601
+ fi
602
+ set +e
603
+ raw="$(node "$probe" --version "$v" --fixture --json 2>&1)"
604
+ local rc=$?
605
+ set -e
606
+ case "$raw" in
607
+ *'"status":"PASS"'*) status="PASS" ;;
608
+ *'"status":"NOT_TESTED"'*) status="NOT_TESTED" ;;
609
+ *'"status":"AMBIENTAL"'*) status="AMBIENTAL" ;;
610
+ *) status="FAIL" ;;
611
+ esac
612
+ detail="$(printf '%s' "$raw" | tr '\n' ' ' | cut -c1-400)"
613
+ if [ "$rc" -ne 0 ] && [ "$status" = "PASS" ]; then
614
+ status="FAIL"
615
+ detail="$detail (probe exit $rc)"
616
+ elif [ "$rc" -ne 0 ] && [ "$status" != "FAIL" ]; then
617
+ detail="$detail (probe exit $rc)"
618
+ fi
619
+ record "$v" "pantheon-cost" "$status" "$detail"
620
+ }
621
+
622
+ run_rehydrate_probe() { # v
623
+ local v="$1" package_root probe raw status detail
624
+ if ! package_root="$(installed_package_root)"; then
625
+ record "$v" "context-rehydrate" "NOT_TESTED" "sandbox package is not installed"
626
+ return 0
627
+ fi
628
+ probe="$package_root/scripts/probe-context-rehydrate.mjs"
629
+ if [ ! -f "$probe" ]; then
630
+ record "$v" "context-rehydrate" "NOT_TESTED" "installed package has no offline context probe"
631
+ return 0
632
+ fi
633
+ if ! command -v node >/dev/null 2>&1; then
634
+ record "$v" "context-rehydrate" "AMBIENTAL" "node runtime is not available"
635
+ return 0
636
+ fi
637
+ set +e
638
+ raw="$(timeout 30 node "$probe" --version "$v" --json 2>&1)"
639
+ local rc=$?
640
+ set -e
641
+ case "$raw" in
642
+ *'"status":"PASS"'*) status="PASS" ;;
643
+ *'"status":"NOT_TESTED"'*) status="NOT_TESTED" ;;
644
+ *'"status":"AMBIENTAL"'*) status="AMBIENTAL" ;;
645
+ *) status="FAIL" ;;
646
+ esac
647
+ detail="$(printf '%s' "$raw" | tr '\n' ' ' | cut -c1-400)"
648
+ if [ "$rc" -eq 124 ]; then
649
+ status="AMBIENTAL"
650
+ detail="context probe timed out after 30s"
651
+ elif [ "$rc" -ne 0 ] && [ "$status" = "PASS" ]; then
652
+ # A non-zero probe exit must never be reported as PASS.
653
+ status="FAIL"
654
+ detail="$detail (probe exit $rc)"
655
+ elif [ "$rc" -ne 0 ] && [ "$status" != "FAIL" ]; then
656
+ detail="$detail (probe exit $rc)"
657
+ fi
658
+ record "$v" "context-rehydrate" "$status" "$detail"
659
+ }
660
+
661
+ run_version_prompts() { # v
662
+ local v="$1" bin
663
+ if ! bin="$(sandbox_bin_v "$v")"; then
664
+ log "Binary '$(bin_for "$v")' not in sandbox prefix — marking all $v prompts AMBIENTAL."
665
+ build_battery "$v"
666
+ local i
667
+ for i in "${!PROMPT_IDS[@]}"; do
668
+ record "$v" "${PROMPT_IDS[$i]}" "AMBIENTAL" "binary $(bin_for "$v") not installed in sandbox"
669
+ done
670
+ record "$v" "mcp-list" "AMBIENTAL" "binary $(bin_for "$v") not installed in sandbox"
671
+ record "$v" "doctor" "AMBIENTAL" "binary $(bin_for "$v") not installed in sandbox"
672
+ return 0
673
+ fi
674
+ prepare_project "$v"
675
+ build_battery "$v"
676
+ local i
677
+ for i in "${!PROMPT_IDS[@]}"; do
678
+ log "[$v] prompt ${PROMPT_IDS[$i]} ..."
679
+ run_prompt "$v" "$i" "$bin"
680
+ log "[$v] prompt ${PROMPT_IDS[$i]} → ${RESULTS["$v:${PROMPT_IDS[$i]}"]}"
681
+ done
682
+ check_mcp_list "$v"
683
+ check_doctor "$v"
684
+ }
685
+
686
+ probe_verdict() { # fail pass ambiental not_tested → one final status
687
+ # Verdict: PASS; Verdict: FAIL; Verdict: AMBIENTAL; Verdict: NOT_TESTED remain distinct.
688
+ local fail="$1" pass="$2" ambiental="$3" not_tested="$4"
689
+ if [ "$fail" -gt 0 ]; then
690
+ printf 'FAIL'
691
+ elif [ "$not_tested" -gt 0 ]; then
692
+ printf 'NOT_TESTED'
693
+ elif [ "$ambiental" -gt 0 ]; then
694
+ printf 'AMBIENTAL'
695
+ else
696
+ printf 'PASS'
697
+ fi
698
+ }
699
+
700
+ write_report() { # versions...
701
+ local versions=("$@") total_fail=0 total_pass=0 total_amb=0 total_not_tested=0
702
+ {
703
+ printf '# Pantheon Sandbox Prompts Report\n\n'
704
+ printf -- '- **Date:** %s\n' "$(date -u '+%Y-%m-%d %H:%M UTC')"
705
+ printf -- '- **Model:** %s\n' "$PANTHEON_SANDBOX_MODEL"
706
+ printf -- '- **Sandbox:** %s\n' "$SANDBOX_ROOT"
707
+ printf -- '- **V1 binary:** %s (%s) | **V2 binary:** %s (%s)\n\n' \
708
+ "$V1_BIN" "$OPENCODE_V1_SPEC" "$V2_BIN" "$OPENCODE_V2_SPEC"
709
+ local v id res
710
+ for v in "${versions[@]}"; do
711
+ printf '## Version %s (`%s`)\n\n' "$v" "$(bin_for "$v")"
712
+ printf '| Prompt | Result | Detail |\n|---|---|---|\n'
713
+ for id in "${PROMPT_IDS[@]}" mcp-list doctor; do
714
+ res="${RESULTS["$v:$id"]:-SKIP}"
715
+ case "$res" in
716
+ PASS) total_pass=$((total_pass + 1)) ;;
717
+ AMBIENTAL) total_amb=$((total_amb + 1)) ;;
718
+ FAIL) total_fail=$((total_fail + 1)) ;;
719
+ NOT_TESTED) total_not_tested=$((total_not_tested + 1)) ;;
720
+ esac
721
+ printf '| %s | %s | %s |\n' "$id" "$res" "${DETAILS["$v:$id"]:-—}"
722
+ done
723
+ printf '\n'
724
+ done
725
+ printf '## Summary\n\n'
726
+ printf -- '- PASS: %d\n- FAIL (real): %d\n- AMBIENTAL: %d\n- NOT_TESTED: %d\n- Retries used: %d\n\n' \
727
+ "$total_pass" "$total_fail" "$total_amb" "$total_not_tested" "$RETRIES_USED"
728
+ printf '**Verdict: %s**\n' "$(probe_verdict "$total_fail" "$total_pass" "$total_amb" "$total_not_tested")"
729
+ } > "$REPORT_FILE"
730
+ log "Report written to $REPORT_FILE"
731
+ }
732
+
733
+ write_cost_report() { # versions...
734
+ local versions=("$@") total_fail=0 total_pass=0 total_ambient=0 total_not_tested=0
735
+ {
736
+ printf '# Pantheon Cost Probe Report\n\n'
737
+ printf -- '- **Date:** %s\n' "$(date -u '+%Y-%m-%d %H:%M UTC')"
738
+ printf -- '- **Sandbox:** %s\n\n' "$SANDBOX_ROOT"
739
+ printf '| Version | Result | Detail |\n|---|---|---|\n'
740
+ local v result
741
+ for v in "${versions[@]}"; do
742
+ result="${RESULTS["$v:pantheon-cost"]:-NOT_TESTED}"
743
+ case "$result" in
744
+ PASS) total_pass=$((total_pass + 1)) ;;
745
+ FAIL) total_fail=$((total_fail + 1)) ;;
746
+ AMBIENTAL) total_ambient=$((total_ambient + 1)) ;;
747
+ NOT_TESTED) total_not_tested=$((total_not_tested + 1)) ;;
748
+ esac
749
+ printf '| %s | %s | %s |\n' "$v" "$result" "${DETAILS["$v:pantheon-cost"]:-—}"
750
+ done
751
+ printf '\n## Summary\n\n'
752
+ printf -- '- PASS: %d\n- FAIL (real): %d\n- AMBIENTAL: %d\n- NOT_TESTED: %d\n\n' \
753
+ "$total_pass" "$total_fail" "$total_ambient" "$total_not_tested"
754
+ printf '**Verdict: %s**\n' "$(probe_verdict "$total_fail" "$total_pass" "$total_ambient" "$total_not_tested")"
755
+ } > "$COST_REPORT_FILE"
756
+ log "Cost probe report written to $COST_REPORT_FILE"
757
+ }
758
+
759
+ write_rehydrate_report() { # versions...
760
+ local versions=("$@") total_fail=0 total_pass=0 total_ambient=0 total_not_tested=0
761
+ {
762
+ printf '# Pantheon Context Rehydration Probe Report\n\n'
763
+ printf -- '- **Date:** %s\n' "$(date -u '+%Y-%m-%d %H:%M UTC')"
764
+ printf -- '- **Sandbox:** %s\n\n' "$SANDBOX_ROOT"
765
+ printf '| Version | Probe | Result | Detail |\n|---|---|---|---|\n'
766
+ local v result
767
+ for v in "${versions[@]}"; do
768
+ result="${RESULTS["$v:context-rehydrate"]:-NOT_TESTED}"
769
+ case "$result" in
770
+ PASS) total_pass=$((total_pass + 1)) ;;
771
+ FAIL) total_fail=$((total_fail + 1)) ;;
772
+ AMBIENTAL) total_ambient=$((total_ambient + 1)) ;;
773
+ NOT_TESTED) total_not_tested=$((total_not_tested + 1)) ;;
774
+ esac
775
+ printf '| %s | context_rehydrate + context_session_summary | %s | %s |\n' \
776
+ "$v" "$result" "${DETAILS["$v:context-rehydrate"]:-—}"
777
+ done
778
+ printf '\n## Summary\n\n'
779
+ printf -- '- PASS: %d\n- FAIL (real): %d\n- AMBIENTAL: %d\n- NOT_TESTED: %d\n\n' \
780
+ "$total_pass" "$total_fail" "$total_ambient" "$total_not_tested"
781
+ printf '**Verdict: %s**\n' "$(probe_verdict "$total_fail" "$total_pass" "$total_ambient" "$total_not_tested")"
782
+ } > "$REHYDRATE_REPORT_FILE"
783
+ log "Context probe report written to $REHYDRATE_REPORT_FILE"
784
+ }
785
+
786
+ run_cost() { # versions...
787
+ local versions=("$@") v fail=0
788
+ mkdir -p "$SANDBOX_ROOT"
789
+ for v in "${versions[@]}"; do
790
+ log "[$v] pantheon_cost offline probe ..."
791
+ run_cost_probe "$v"
792
+ log "[$v] pantheon-cost → ${RESULTS["$v:pantheon-cost"]}"
793
+ done
794
+ write_cost_report "${versions[@]}"
795
+ for v in "${versions[@]}"; do
796
+ [ "${RESULTS["$v:pantheon-cost"]:-}" = "FAIL" ] && fail=$((fail + 1)) || true
797
+ done
798
+ if [ "$fail" -gt 0 ]; then
799
+ log "Cost probe: $fail real FAIL(s)."
800
+ exit 1
801
+ fi
802
+ log "Cost probe: no real failures (NOT_TESTED/AMBIENTAL is non-blocking)."
803
+ }
804
+
805
+ run_rehydrate() { # versions...
806
+ local versions=("$@") v fail=0
807
+ mkdir -p "$SANDBOX_ROOT"
808
+ for v in "${versions[@]}"; do
809
+ log "[$v] context_rehydrate/session_summary offline probe ..."
810
+ run_rehydrate_probe "$v"
811
+ log "[$v] context probe → ${RESULTS["$v:context-rehydrate"]}"
812
+ done
813
+ write_rehydrate_report "${versions[@]}"
814
+ for v in "${versions[@]}"; do
815
+ [ "${RESULTS["$v:context-rehydrate"]:-}" = "FAIL" ] && fail=$((fail + 1)) || true
816
+ done
817
+ if [ "$fail" -gt 0 ]; then
818
+ log "Context probe: $fail real FAIL(s)."
819
+ exit 1
820
+ fi
821
+ log "Context probe: no real failures (NOT_TESTED/AMBIENTAL is non-blocking)."
822
+ }
823
+
824
+ run_battery() { # versions...
825
+ local versions=("$@")
826
+ mkdir -p "$SANDBOX_ROOT"
827
+ write_extract_py
828
+ local v
829
+ for v in "${versions[@]}"; do
830
+ run_version_prompts "$v"
831
+ done
832
+ write_report "${versions[@]}"
833
+ local v fail=0 id
834
+ for v in "${versions[@]}"; do
835
+ for id in "${PROMPT_IDS[@]}" mcp-list doctor; do
836
+ [ "${RESULTS["$v:$id"]:-}" = "FAIL" ] && fail=$((fail + 1)) || true
837
+ done
838
+ done
839
+ if [ "$fail" -gt 0 ]; then
840
+ log "Prompts battery: $fail real FAIL(s)."
841
+ exit 1
842
+ fi
843
+ log "Prompts battery: no real failures."
844
+ }
845
+
846
+ # ── Base validation (no prompts) ──────────────────────────────────────────────
847
+
848
+ run_base() { # v
849
+ local v="$1" bin
850
+ if ! bin="$(sandbox_bin_v "$v")"; then
851
+ printf 'ERROR: sandbox not prepared for %s (binary '\''%s'\'' missing from sandbox prefix) — run %s --prepare first\n' \
852
+ "$v" "$(bin_for "$v")" "$0" >&2
853
+ exit 3
854
+ fi
855
+ prepare_project "$v"
856
+ build_battery "$v"
857
+ log "[$v] opencode mcp list"
858
+ check_mcp_list "$v"
859
+ log "[$v] mcp-list → ${RESULTS["$v:mcp-list"]}"
860
+ log "[$v] doctor"
861
+ check_doctor "$v"
862
+ log "[$v] doctor → ${RESULTS["$v:doctor"]}"
863
+ local fail=0
864
+ [ "${RESULTS["$v:mcp-list"]}" = "FAIL" ] && fail=$((fail + 1)) || true
865
+ [ "${RESULTS["$v:doctor"]}" = "FAIL" ] && fail=$((fail + 1)) || true
866
+ if [ "$fail" -gt 0 ]; then
867
+ log "Base validation $v: $fail real FAIL(s)."
868
+ exit 1
869
+ fi
870
+ log "Base validation $v: OK."
871
+ }
872
+
873
+ # ── CLI ───────────────────────────────────────────────────────────────────────
874
+
875
+ while [ $# -gt 0 ]; do
876
+ case "$1" in
877
+ --prepare) MODE_PREPARE=1 ;;
878
+ --reset) MODE_RESET=1 ;;
879
+ --prompts) MODE_PROMPTS=1 ;;
880
+ --cost) MODE_COST=1 ;;
881
+ --rehydrate) MODE_REHYDRATE=1 ;;
882
+ --run)
883
+ shift
884
+ case "${1:-}" in
885
+ v1 | v2) RUN_VERSION="$1" ;;
886
+ *) usage ;;
887
+ esac
888
+ ;;
889
+ --help | -h) usage ;;
890
+ *) usage ;;
891
+ esac
892
+ shift
893
+ done
894
+
895
+ if [ "$MODE_RESET" -eq 1 ]; then
896
+ cmd_reset
897
+ exit 0
898
+ fi
899
+
900
+ if [ "$MODE_PREPARE" -eq 0 ] && [ -z "$RUN_VERSION" ] \
901
+ && [ "$MODE_PROMPTS" -eq 0 ] && [ "$MODE_COST" -eq 0 ] \
902
+ && [ "$MODE_REHYDRATE" -eq 0 ]; then
903
+ usage
904
+ fi
905
+
906
+ if [ "$MODE_PREPARE" -eq 1 ]; then
907
+ cmd_prepare
908
+ fi
909
+
910
+ if [ -n "$RUN_VERSION" ]; then
911
+ if [ "$MODE_PROMPTS" -eq 1 ]; then
912
+ sandbox_env
913
+ run_battery "$RUN_VERSION"
914
+ else
915
+ sandbox_env
916
+ run_base "$RUN_VERSION"
917
+ fi
918
+ if [ "$MODE_COST" -eq 1 ]; then
919
+ sandbox_env
920
+ run_cost "$RUN_VERSION"
921
+ fi
922
+ if [ "$MODE_REHYDRATE" -eq 1 ]; then
923
+ sandbox_env
924
+ run_rehydrate "$RUN_VERSION"
925
+ fi
926
+ elif [ "$MODE_PROMPTS" -eq 1 ]; then
927
+ sandbox_env
928
+ run_battery v1 v2
929
+ if [ "$MODE_COST" -eq 1 ]; then
930
+ run_cost v1 v2
931
+ fi
932
+ if [ "$MODE_REHYDRATE" -eq 1 ]; then
933
+ run_rehydrate v1 v2
934
+ fi
935
+ elif [ "$MODE_COST" -eq 1 ]; then
936
+ sandbox_env
937
+ run_cost v1 v2
938
+ if [ "$MODE_REHYDRATE" -eq 1 ]; then
939
+ run_rehydrate v1 v2
940
+ fi
941
+ elif [ "$MODE_REHYDRATE" -eq 1 ]; then
942
+ sandbox_env
943
+ run_rehydrate v1 v2
944
+ fi