pantheon-opencode 1.4.2 → 1.5.0-beta.1

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 (98) hide show
  1. package/.pantheon/code-mode/.gitkeep +0 -0
  2. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-1.json +8 -0
  3. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-10.json +8 -0
  4. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-11.json +8 -0
  5. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-2.json +8 -0
  6. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-3.json +8 -0
  7. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-4.json +8 -0
  8. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-5.json +8 -0
  9. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-6.json +8 -0
  10. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-7.json +8 -0
  11. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-8.json +8 -0
  12. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-9.json +8 -0
  13. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/heartbeat.json +8 -0
  14. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/session.json +11 -0
  15. package/.pantheon/code-mode/README.md +93 -0
  16. package/.pantheon/code-mode/checkpoint-session.py +7 -0
  17. package/.pantheon/code-mode/checkpoint-session.sh +11 -0
  18. package/.pantheon/code-mode/checkpoint_session.py +481 -0
  19. package/.pantheon/code-mode/compress-inline.py +450 -0
  20. package/.pantheon/code-mode/eval-llm-judge.py +255 -0
  21. package/.pantheon/code-mode/eval-monte-carlo.py +368 -0
  22. package/.pantheon/code-mode/eval-run.py +197 -0
  23. package/.pantheon/code-mode/eval-static.py +321 -0
  24. package/.pantheon/code-mode/example-sync.sh +5 -0
  25. package/.pantheon/code-mode/session-end-save.py +186 -0
  26. package/.pantheon/code-mode/session-end-save.sh +18 -0
  27. package/.pantheon/code-mode/test-allowed.sh +5 -0
  28. package/.pantheon/code-mode/test-auto-continue-checkpoint.sh +179 -0
  29. package/.pantheon/code-mode/test-env.sh +7 -0
  30. package/.pantheon/code-mode/test-prlimit.sh +9 -0
  31. package/.pantheon/code-mode/test-timeout.sh +6 -0
  32. package/README.md +158 -1189
  33. package/README.pt-BR.md +222 -0
  34. package/bin/pantheon-init.mjs +30 -11
  35. package/opencode.json +1 -0
  36. package/package.json +14 -3
  37. package/plugin.json +15 -0
  38. package/scripts/_pantheon_paths.py +48 -7
  39. package/scripts/ci-validate-yaml.py +7 -2
  40. package/scripts/code_mode_server.py +33 -2
  41. package/scripts/doctor.mjs +135 -127
  42. package/scripts/generate-preset-docs.mjs +127 -66
  43. package/scripts/generate-prompts.sh +2 -2
  44. package/scripts/install/cli-ui.mjs +8 -1
  45. package/scripts/install/config-migration.mjs +401 -0
  46. package/scripts/install/health-check.mjs +3 -3
  47. package/scripts/install/model-picker.mjs +72 -23
  48. package/scripts/install/opencode-version.mjs +47 -0
  49. package/scripts/install/opencode.mjs +261 -88
  50. package/scripts/install/plugin.mjs +112 -42
  51. package/scripts/install/shared.mjs +13 -158
  52. package/scripts/install-mcp.mjs +44 -232
  53. package/scripts/mcp_persistence_server.py +344 -15
  54. package/scripts/mcp_resources_server.py +144 -40
  55. package/scripts/memory_mcp_server.py +2 -2
  56. package/scripts/recover-zenodo-deposition.mjs +146 -0
  57. package/scripts/sync-tui.mjs +32 -7
  58. package/scripts/test-opencode-v1-v2-sandbox.sh +591 -0
  59. package/scripts/uninstall.mjs +298 -954
  60. package/scripts/validate-zenodo-release.mjs +154 -0
  61. package/scripts/zenodo-artifact.mjs +21 -0
  62. package/scripts/zenodo-release-state.mjs +53 -0
  63. package/src/mcp/_pantheon_paths.py +48 -7
  64. package/src/mcp/code_mode_server.py +33 -2
  65. package/src/mcp/eval_store.py +1 -1
  66. package/src/mcp/mcp_persistence_server.py +344 -15
  67. package/src/mcp/mcp_resources_server.py +144 -40
  68. package/src/mcp/pantheon_vision_server.py +1 -1
  69. package/src/mcp/toon_codec.py +491 -0
  70. package/src/pantheon/background-job-board.ts +67 -1
  71. package/src/pantheon/cost-command.ts +6 -2
  72. package/src/pantheon/delegate-manager.ts +384 -0
  73. package/src/pantheon/delegate-task-adapter.ts +444 -0
  74. package/src/pantheon/delegation-notify.ts +35 -20
  75. package/src/pantheon/model-command.ts +86 -32
  76. package/src/pantheon/token-opt.ts +555 -0
  77. package/src/pantheon/toon-codec.ts +481 -0
  78. package/src/pantheon/tui-version-check.ts +1 -3
  79. package/src/pantheon/v2-bridge.ts +129 -0
  80. package/src/pantheon/v2-events.ts +131 -0
  81. package/src/pantheon/v2-hooks.ts +287 -0
  82. package/src/pantheon/v2-tool-definitions.ts +331 -0
  83. package/src/plugin-v2/index.ts +21 -0
  84. package/src/plugin-v2.ts +836 -0
  85. package/src/plugin.ts +57 -10
  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 +22 -16
  98. package/src/skills/README.md +0 -2
@@ -0,0 +1,591 @@
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
+ # --reset Wipe the sandbox root.
17
+ # --help Usage.
18
+ #
19
+ # Env overrides:
20
+ # PANTHEON_SANDBOX_ROOT Sandbox root (default: ~/pantheon-sandbox)
21
+ # OPENCODE_V1_SPEC npm spec providing the `opencode` binary
22
+ # (default: opencode-ai@1.18.18)
23
+ # OPENCODE_V2_SPEC npm spec providing the `opencode2` binary
24
+ # (default: @opencode-ai/cli@beta)
25
+ # PANTHEON_SANDBOX_MODEL Model used by init/prompts
26
+ # (default: opencode-go/mimo-v2.5)
27
+ # PANTHEON_PROMPT_TIMEOUT Per-prompt timeout in seconds (default: 300)
28
+ # PANTHEON_RETRY_COOLDOWN Cooldown seconds before the single FAIL retry
29
+ # (default: 60; retry only for real FAIL, never AMBIENTAL)
30
+ #
31
+ # Exit codes: 0 = no real failures (AMBIENTAL issues do not fail the run),
32
+ # 1 = real FAIL (see prompts-report.md in the sandbox root),
33
+ # 2 = usage error, 3 = sandbox not prepared.
34
+ set -euo pipefail
35
+
36
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
37
+ REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
38
+
39
+ SANDBOX_ROOT="${PANTHEON_SANDBOX_ROOT:-$HOME/pantheon-sandbox}"
40
+ SANDBOX_HOME="$SANDBOX_ROOT/home"
41
+ NPM_PREFIX="$SANDBOX_HOME/.npm-global"
42
+
43
+ OPENCODE_V1_SPEC="${OPENCODE_V1_SPEC:-opencode-ai@1.18.18}"
44
+ OPENCODE_V2_SPEC="${OPENCODE_V2_SPEC:-@opencode-ai/cli@beta}"
45
+ PANTHEON_SANDBOX_MODEL="${PANTHEON_SANDBOX_MODEL:-opencode-go/mimo-v2.5}"
46
+ PANTHEON_PROMPT_TIMEOUT="${PANTHEON_PROMPT_TIMEOUT:-300}"
47
+
48
+ V1_BIN="${OPENCODE_V1_BIN:-opencode}"
49
+ V2_BIN="${OPENCODE_V2_BIN:-opencode2}"
50
+
51
+ REPORT_FILE="$SANDBOX_ROOT/prompts-report.md"
52
+ EXTRACT_PY="$SANDBOX_ROOT/.prompt-extract-json.py"
53
+
54
+ MODE_PREPARE=0
55
+ MODE_RESET=0
56
+ MODE_PROMPTS=0
57
+ RUN_VERSION=""
58
+
59
+ usage() {
60
+ sed -n '2,40p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
61
+ exit 2
62
+ }
63
+
64
+ log() { printf '%s\n' "$*"; }
65
+ warn() { printf 'WARN: %s\n' "$*" >&2; }
66
+ die() { printf 'ERROR: %s\n' "$*" >&2; exit 1; }
67
+
68
+ bin_for() {
69
+ case "$1" in
70
+ v1) printf '%s' "$V1_BIN" ;;
71
+ v2) printf '%s' "$V2_BIN" ;;
72
+ *) return 1 ;;
73
+ esac
74
+ }
75
+
76
+ project_for() { printf '%s/project-%s' "$SANDBOX_ROOT" "$1"; }
77
+
78
+ # Resolve a binary STRICTLY inside the sandbox npm prefix — never fall back to
79
+ # the dev PATH, otherwise a non-prepared sandbox would silently test the host
80
+ # installation.
81
+ sandbox_bin_for() { # name → prints path; rc 1 if not inside sandbox prefix
82
+ local p
83
+ p="$(command -v "$1" 2>/dev/null || true)"
84
+ case "$p" in
85
+ "$NPM_PREFIX"/*) printf '%s' "$p" ;;
86
+ *) return 1 ;;
87
+ esac
88
+ }
89
+
90
+ sandbox_bin_v() { # v → echoes path; rc 1 if missing
91
+ sandbox_bin_for "$(bin_for "$1")"
92
+ }
93
+
94
+ # ── Sandbox environment isolation ─────────────────────────────────────────────
95
+
96
+ sandbox_env() {
97
+ export HOME="$SANDBOX_HOME"
98
+ export PATH="$NPM_PREFIX/bin:$HOME/.config/opencode/.venv/bin:$PATH"
99
+ export npm_config_prefix="$NPM_PREFIX"
100
+ }
101
+
102
+ # ── Gate (b): sandbox run-test.sh with pantheon://agents content validation ──
103
+
104
+ write_run_test_sh() {
105
+ cat > "$SANDBOX_ROOT/run-test.sh" <<'RUNTEST'
106
+ #!/usr/bin/env bash
107
+ # Generated by pantheon-opencode scripts/test-opencode-v1-v2-sandbox.sh --prepare
108
+ # Validates the global install: binaries, 5/5 MCP connectivity, doctor 0 errors,
109
+ # AND the CONTENT of the pantheon://agents resource (zeus/hermes present).
110
+ set -euo pipefail
111
+
112
+ SANDBOX_DIR="$(cd "$(dirname "$0")" && pwd)"
113
+ SANDBOX_HOME="$SANDBOX_DIR/home"
114
+ NPM_PREFIX="$SANDBOX_HOME/.npm-global"
115
+ PANTHEON_GLOBAL="$SANDBOX_HOME/.config/opencode"
116
+ SANDBOX_VENV="$PANTHEON_GLOBAL/.venv"
117
+ REPO_DIR="${PANTHEON_REPO:-$(cd "$SANDBOX_DIR/../pantheon" && pwd)}"
118
+
119
+ echo "=== Pantheon Sandbox Test ==="
120
+ echo "Sandbox: $SANDBOX_DIR"
121
+ echo "Repo: $REPO_DIR"
122
+
123
+ export HOME="$SANDBOX_HOME"
124
+ export PATH="$NPM_PREFIX/bin:$SANDBOX_VENV/bin:$PATH"
125
+ export npm_config_prefix="$NPM_PREFIX"
126
+
127
+ echo "--- Binaries ---"
128
+ opencode --version
129
+ opencode2 --version
130
+
131
+ echo "--- Building tarball ---"
132
+ cd "$REPO_DIR"
133
+ npm pack 2>/dev/null || { echo "ERROR: npm pack failed"; exit 1; }
134
+ TGZ=$(ls -t pantheon-opencode-*.tgz | head -1)
135
+ npm rm -g pantheon-opencode 2>/dev/null || true
136
+ npm install -g "$TGZ"
137
+
138
+ echo "--- Init (headless) ---"
139
+ pantheon-opencode init --headless -y
140
+
141
+ echo "--- MCP List (V1) ---"
142
+ opencode mcp list
143
+
144
+ echo "--- MCP List (V2) ---"
145
+ opencode2 mcp list
146
+
147
+ echo "--- Doctor ---"
148
+ pantheon-opencode doctor --target "$SANDBOX_DIR"
149
+
150
+ echo "--- Gate (b): pantheon://agents CONTENT check ---"
151
+ RESOURCES_SRV="$PANTHEON_GLOBAL/scripts/mcp_resources_server.py"
152
+ if [ ! -f "$RESOURCES_SRV" ]; then
153
+ echo "ERROR: resources server not found at $RESOURCES_SRV"; exit 1
154
+ fi
155
+ AGENTS_CONTENT="$("$SANDBOX_VENV/bin/python" - "$RESOURCES_SRV" <<'PYEOF'
156
+ import asyncio, importlib.util, sys
157
+ spec = importlib.util.spec_from_file_location("mrs", sys.argv[1])
158
+ mod = importlib.util.module_from_spec(spec)
159
+ spec.loader.exec_module(mod)
160
+ print(asyncio.run(mod.list_agents()))
161
+ PYEOF
162
+ )"
163
+ printf '%s\n' "$AGENTS_CONTENT" | head -8
164
+ if printf '%s\n' "$AGENTS_CONTENT" | grep -qi 'zeus' \
165
+ && printf '%s\n' "$AGENTS_CONTENT" | grep -qi 'hermes'; then
166
+ echo "OK: pantheon://agents content includes zeus and hermes"
167
+ else
168
+ echo "ERROR: pantheon://agents content missing zeus/hermes (connectivity alone is not enough)"; exit 1
169
+ fi
170
+
171
+ echo ""
172
+ echo "=== VALIDATION COMPLETE ==="
173
+ echo "Next: open the isolated TUI with: $SANDBOX_DIR/start-pantheon.sh"
174
+ RUNTEST
175
+ chmod +x "$SANDBOX_ROOT/run-test.sh"
176
+ }
177
+
178
+ # ── Reset / Prepare ───────────────────────────────────────────────────────────
179
+
180
+ cmd_reset() {
181
+ case "$SANDBOX_ROOT" in
182
+ "/" | "$HOME" | "$REPO_DIR" | "$REPO_DIR"/*)
183
+ die "refusing to reset unsafe sandbox root: $SANDBOX_ROOT" ;;
184
+ esac
185
+ log "Resetting sandbox: $SANDBOX_ROOT"
186
+ rm -rf "$SANDBOX_ROOT"
187
+ log "Sandbox removed."
188
+ }
189
+
190
+ install_binaries() {
191
+ log "--- Installing pantheon-opencode (from repo tarball) ---"
192
+ cd "$REPO_DIR"
193
+ npm pack 2>/dev/null || die "npm pack failed"
194
+ local tgz
195
+ tgz=$(ls -t pantheon-opencode-*.tgz | head -1)
196
+ npm rm -g pantheon-opencode 2>/dev/null || true
197
+ npm install -g "$REPO_DIR/$tgz" || die "install of $tgz failed"
198
+ log "--- Installing OpenCode V1 ($OPENCODE_V1_SPEC) ---"
199
+ npm install -g "$OPENCODE_V1_SPEC" \
200
+ || warn "V1 binary install failed ($OPENCODE_V1_SPEC) — v1 prompts will be AMBIENTAL"
201
+ log "--- Installing OpenCode V2 ($OPENCODE_V2_SPEC) ---"
202
+ npm install -g "$OPENCODE_V2_SPEC" \
203
+ || warn "V2 binary install failed ($OPENCODE_V2_SPEC) — v2 prompts will be AMBIENTAL"
204
+ }
205
+
206
+ prepare_project() {
207
+ local v="$1" dir pan
208
+ dir="$(project_for "$v")"
209
+ mkdir -p "$dir"
210
+ pan="$(sandbox_bin_for "pantheon-opencode")" \
211
+ || die "pantheon-opencode not installed in sandbox prefix — run $0 --prepare first"
212
+ log "--- Regenerating config for $v in $dir ---"
213
+ (cd "$dir" && "$pan" init --project --version "$v" --model "$PANTHEON_SANDBOX_MODEL" -y) \
214
+ || die "init --project --version $v failed in $dir"
215
+ }
216
+
217
+ cmd_prepare() {
218
+ mkdir -p "$SANDBOX_HOME" "$NPM_PREFIX"
219
+ write_run_test_sh
220
+ sandbox_env
221
+ install_binaries
222
+ local pan
223
+ pan="$(sandbox_bin_for "pantheon-opencode")" \
224
+ || die "pantheon-opencode not found in sandbox prefix after install"
225
+ log "--- Headless init (global venv + runtime) ---"
226
+ "$pan" init --headless -y --model "$PANTHEON_SANDBOX_MODEL" \
227
+ || die "pantheon-opencode init --headless failed"
228
+ prepare_project v1
229
+ prepare_project v2
230
+ write_extract_py
231
+ log "Prepare complete. Sandbox: $SANDBOX_ROOT"
232
+ log "Next: $0 --run v1 --prompts | $0 --run v2 --prompts"
233
+ }
234
+
235
+ # ── JSON text extraction (schema-agnostic) ────────────────────────────────────
236
+
237
+ write_extract_py() {
238
+ cat > "$EXTRACT_PY" <<'PYEOF'
239
+ """Collect every string value from opencode run --format json output."""
240
+ import json
241
+ import sys
242
+
243
+
244
+ def walk(obj, out):
245
+ if isinstance(obj, str):
246
+ out.append(obj)
247
+ elif isinstance(obj, dict):
248
+ for value in obj.values():
249
+ walk(value, out)
250
+ elif isinstance(obj, list):
251
+ for value in obj:
252
+ walk(value, out)
253
+
254
+
255
+ raw = sys.stdin.read()
256
+ chunks = []
257
+ try:
258
+ walk(json.loads(raw), chunks)
259
+ except Exception:
260
+ for line in raw.splitlines():
261
+ line = line.strip()
262
+ if not line:
263
+ continue
264
+ try:
265
+ walk(json.loads(line), chunks)
266
+ except Exception:
267
+ chunks.append(line)
268
+ print("\n".join(chunks))
269
+ PYEOF
270
+ }
271
+
272
+ # ── Prompt battery ────────────────────────────────────────────────────────────
273
+
274
+ # Known environmental failure signatures (never fail the run).
275
+ 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'
276
+
277
+ PROMPT_IDS=()
278
+ PROMPT_TEXTS=()
279
+ PROMPT_MARKERS=()
280
+ PROMPT_AMB_MARKERS=()
281
+ PROMPT_VERIFY=()
282
+
283
+ add_prompt() { # id marker ambiental_marker verify text
284
+ PROMPT_IDS+=("$1")
285
+ PROMPT_MARKERS+=("$2")
286
+ PROMPT_AMB_MARKERS+=("$3")
287
+ PROMPT_VERIFY+=("$4")
288
+ PROMPT_TEXTS+=("$5")
289
+ }
290
+
291
+ build_battery() {
292
+ local v="$1"
293
+ PROMPT_IDS=()
294
+ PROMPT_TEXTS=()
295
+ PROMPT_MARKERS=()
296
+ PROMPT_AMB_MARKERS=()
297
+ PROMPT_VERIFY=()
298
+ add_prompt "agents-resource" "AGENTS-OK" "AGENTS-MISSING" "" \
299
+ "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."
300
+ add_prompt "memory-store" "MEMORY-STORE-OK" "" "" \
301
+ "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"
302
+ add_prompt "memory-recall" "MEMORY-RECALL-OK" "" "" \
303
+ "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"
304
+ add_prompt "tmp-write-read" "TMP-OK" "" \
305
+ "${TMPDIR:-/tmp}/pantheon-sandbox-probe-$v.txt::TMP-OK" \
306
+ "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"
307
+ add_prompt "delegation" "DELEGATION-OK" "DELEGATION-UNAVAILABLE" "" \
308
+ "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"
309
+ }
310
+
311
+ declare -A RESULTS
312
+ declare -A DETAILS
313
+
314
+ record() { # v id result detail
315
+ RESULTS["$1:$2"]="$3"
316
+ DETAILS["$1:$2"]="$4"
317
+ }
318
+
319
+ extract_text() { "$HOME/.config/opencode/.venv/bin/python" "$EXTRACT_PY" < "$1"; }
320
+
321
+ ATTEMPT_RESULT=""
322
+ ATTEMPT_DETAIL=""
323
+
324
+ run_prompt_attempt() { # v idx current_bin — sets ATTEMPT_RESULT / ATTEMPT_DETAIL
325
+ local v="$1" idx="$2" bin="$3" id marker amb_marker verify text
326
+ id="${PROMPT_IDS[$idx]}"
327
+ marker="${PROMPT_MARKERS[$idx]}"
328
+ amb_marker="${PROMPT_AMB_MARKERS[$idx]}"
329
+ verify="${PROMPT_VERIFY[$idx]}"
330
+ text="${PROMPT_TEXTS[$idx]}"
331
+
332
+ # Remove the probe file first so post-run verification reflects THIS run.
333
+ if [ -n "$verify" ]; then
334
+ rm -f "${verify%%::*}"
335
+ fi
336
+
337
+ local out_file err_file rc=0
338
+ out_file="$(mktemp)"
339
+ err_file="$(mktemp)"
340
+
341
+ set +e
342
+ (cd "$(project_for "$v")" \
343
+ && timeout "$PANTHEON_PROMPT_TIMEOUT" "$bin" run --auto --format json "$text") \
344
+ > "$out_file" 2> "$err_file"
345
+ rc=$?
346
+ set -e
347
+
348
+ local extracted
349
+ extracted="$(extract_text "$out_file" || true)"
350
+ local raw
351
+ raw="$( { tr '\n' ' ' < "$out_file"; tr '\n' ' ' < "$err_file"; } | cut -c1-400)"
352
+ rm -f "$out_file" "$err_file"
353
+
354
+ if [ "$rc" -eq 124 ]; then
355
+ ATTEMPT_RESULT="AMBIENTAL"
356
+ ATTEMPT_DETAIL="timeout after ${PANTHEON_PROMPT_TIMEOUT}s (provider latency)"
357
+ elif printf '%s' "$extracted" | grep -q "$marker"; then
358
+ ATTEMPT_RESULT="PASS"
359
+ ATTEMPT_DETAIL="marker $marker found"
360
+ elif [ -n "$amb_marker" ] && printf '%s' "$extracted" | grep -q "$amb_marker"; then
361
+ ATTEMPT_RESULT="AMBIENTAL"
362
+ ATTEMPT_DETAIL="explicit $amb_marker"
363
+ elif [ -n "$verify" ] \
364
+ && [ "$(cat "${verify%%::*}" 2>/dev/null)" = "${verify##*::}" ]; then
365
+ ATTEMPT_RESULT="PASS"
366
+ ATTEMPT_DETAIL="probe file verified: ${verify%%::*} contains ${verify##*::}"
367
+ elif printf '%s' "$raw" | grep -Eiq "$AMBIENTAL_RE"; then
368
+ local sig
369
+ sig="$(printf '%s' "$raw" | grep -Eio "$AMBIENTAL_RE" | head -1)"
370
+ ATTEMPT_RESULT="AMBIENTAL"
371
+ ATTEMPT_DETAIL="environmental signature: $sig"
372
+ else
373
+ ATTEMPT_RESULT="FAIL"
374
+ ATTEMPT_DETAIL="marker $marker absent; rc=$rc; snippet: $(printf '%s' "$raw" | cut -c1-160)"
375
+ fi
376
+ }
377
+
378
+ RETRY_COOLDOWN="${PANTHEON_RETRY_COOLDOWN:-60}"
379
+ RETRIES_USED=0
380
+
381
+ run_prompt() { # v idx current_bin — ONE retry after cooldown for real FAIL only
382
+ local v="$1" idx="$2" bin="$3" id first_detail
383
+ id="${PROMPT_IDS[$idx]}"
384
+ run_prompt_attempt "$v" "$idx" "$bin"
385
+ if [ "$ATTEMPT_RESULT" = "FAIL" ]; then
386
+ first_detail="$ATTEMPT_DETAIL"
387
+ log "[$v] prompt $id FAIL on first attempt — retrying once after ${RETRY_COOLDOWN}s cooldown"
388
+ sleep "$RETRY_COOLDOWN"
389
+ run_prompt_attempt "$v" "$idx" "$bin"
390
+ RETRIES_USED=$((RETRIES_USED + 1))
391
+ if [ "$ATTEMPT_RESULT" != "FAIL" ]; then
392
+ ATTEMPT_DETAIL="$ATTEMPT_DETAIL [retry used — first attempt: $first_detail]"
393
+ else
394
+ ATTEMPT_DETAIL="$ATTEMPT_DETAIL [FAIL persisted after retry — first attempt: $first_detail]"
395
+ fi
396
+ fi
397
+ record "$v" "$id" "$ATTEMPT_RESULT" "$ATTEMPT_DETAIL"
398
+ }
399
+
400
+ check_mcp_list() { # v
401
+ local v="$1" bin out rc=0
402
+ if ! bin="$(sandbox_bin_v "$v")"; then
403
+ record "$v" "mcp-list" "AMBIENTAL" "binary not installed in sandbox prefix"
404
+ return 0
405
+ fi
406
+ set +e
407
+ out="$(cd "$(project_for "$v")" && "$bin" mcp list 2>&1)"
408
+ rc=$?
409
+ set -e
410
+ if [ "$rc" -ne 0 ]; then
411
+ record "$v" "mcp-list" "FAIL" "opencode mcp list exited $rc"
412
+ elif printf '%s' "$out" | grep -qi "connected" \
413
+ && ! printf '%s' "$out" | grep -Eiq "failed|✘"; then
414
+ record "$v" "mcp-list" "PASS" "$(printf '%s' "$out" | grep -ci 'connected') connected"
415
+ else
416
+ record "$v" "mcp-list" "FAIL" "no connected MCPs or failures reported"
417
+ fi
418
+ }
419
+
420
+ check_doctor() { # v
421
+ local v="$1" pan rc=0
422
+ if ! pan="$(sandbox_bin_for "pantheon-opencode")"; then
423
+ record "$v" "doctor" "AMBIENTAL" "pantheon-opencode not installed in sandbox prefix"
424
+ return 0
425
+ fi
426
+ set +e
427
+ (cd "$(project_for "$v")" && "$pan" doctor --target "$(project_for "$v")") > /dev/null 2>&1
428
+ rc=$?
429
+ set -e
430
+ if [ "$rc" -eq 0 ]; then
431
+ record "$v" "doctor" "PASS" "exit 0"
432
+ else
433
+ record "$v" "doctor" "FAIL" "doctor exited $rc (blocking errors)"
434
+ fi
435
+ }
436
+
437
+ run_version_prompts() { # v
438
+ local v="$1" bin
439
+ if ! bin="$(sandbox_bin_v "$v")"; then
440
+ log "Binary '$(bin_for "$v")' not in sandbox prefix — marking all $v prompts AMBIENTAL."
441
+ build_battery "$v"
442
+ local i
443
+ for i in "${!PROMPT_IDS[@]}"; do
444
+ record "$v" "${PROMPT_IDS[$i]}" "AMBIENTAL" "binary $(bin_for "$v") not installed in sandbox"
445
+ done
446
+ record "$v" "mcp-list" "AMBIENTAL" "binary $(bin_for "$v") not installed in sandbox"
447
+ record "$v" "doctor" "AMBIENTAL" "binary $(bin_for "$v") not installed in sandbox"
448
+ return 0
449
+ fi
450
+ prepare_project "$v"
451
+ build_battery "$v"
452
+ local i
453
+ for i in "${!PROMPT_IDS[@]}"; do
454
+ log "[$v] prompt ${PROMPT_IDS[$i]} ..."
455
+ run_prompt "$v" "$i" "$bin"
456
+ log "[$v] prompt ${PROMPT_IDS[$i]} → ${RESULTS["$v:${PROMPT_IDS[$i]}"]}"
457
+ done
458
+ check_mcp_list "$v"
459
+ check_doctor "$v"
460
+ }
461
+
462
+ write_report() { # versions...
463
+ local versions=("$@") total_fail=0 total_pass=0 total_amb=0
464
+ {
465
+ printf '# Pantheon Sandbox Prompts Report\n\n'
466
+ printf -- '- **Date:** %s\n' "$(date -u '+%Y-%m-%d %H:%M UTC')"
467
+ printf -- '- **Model:** %s\n' "$PANTHEON_SANDBOX_MODEL"
468
+ printf -- '- **Sandbox:** %s\n' "$SANDBOX_ROOT"
469
+ printf -- '- **V1 binary:** %s (%s) | **V2 binary:** %s (%s)\n\n' \
470
+ "$V1_BIN" "$OPENCODE_V1_SPEC" "$V2_BIN" "$OPENCODE_V2_SPEC"
471
+ local v id res
472
+ for v in "${versions[@]}"; do
473
+ printf '## Version %s (`%s`)\n\n' "$v" "$(bin_for "$v")"
474
+ printf '| Prompt | Result | Detail |\n|---|---|---|\n'
475
+ for id in "${PROMPT_IDS[@]}" mcp-list doctor; do
476
+ res="${RESULTS["$v:$id"]:-SKIP}"
477
+ case "$res" in
478
+ PASS) total_pass=$((total_pass + 1)) ;;
479
+ AMBIENTAL) total_amb=$((total_amb + 1)) ;;
480
+ FAIL) total_fail=$((total_fail + 1)) ;;
481
+ esac
482
+ printf '| %s | %s | %s |\n' "$id" "$res" "${DETAILS["$v:$id"]:-—}"
483
+ done
484
+ printf '\n'
485
+ done
486
+ printf '## Summary\n\n'
487
+ printf -- '- PASS: %d\n- FAIL (real): %d\n- AMBIENTAL: %d\n- Retries used: %d\n\n' \
488
+ "$total_pass" "$total_fail" "$total_amb" "$RETRIES_USED"
489
+ if [ "$total_fail" -eq 0 ]; then
490
+ printf '**Verdict: PASS** — no real failures (AMBIENTAL issues do not block).\n'
491
+ else
492
+ printf '**Verdict: FAIL** — real failures detected (see table above).\n'
493
+ fi
494
+ } > "$REPORT_FILE"
495
+ log "Report written to $REPORT_FILE"
496
+ }
497
+
498
+ run_battery() { # versions...
499
+ local versions=("$@")
500
+ mkdir -p "$SANDBOX_ROOT"
501
+ write_extract_py
502
+ local v
503
+ for v in "${versions[@]}"; do
504
+ run_version_prompts "$v"
505
+ done
506
+ write_report "${versions[@]}"
507
+ local v fail=0 id
508
+ for v in "${versions[@]}"; do
509
+ for id in "${PROMPT_IDS[@]}" mcp-list doctor; do
510
+ [ "${RESULTS["$v:$id"]:-}" = "FAIL" ] && fail=$((fail + 1)) || true
511
+ done
512
+ done
513
+ if [ "$fail" -gt 0 ]; then
514
+ log "Prompts battery: $fail real FAIL(s)."
515
+ exit 1
516
+ fi
517
+ log "Prompts battery: no real failures."
518
+ }
519
+
520
+ # ── Base validation (no prompts) ──────────────────────────────────────────────
521
+
522
+ run_base() { # v
523
+ local v="$1" bin
524
+ if ! bin="$(sandbox_bin_v "$v")"; then
525
+ printf 'ERROR: sandbox not prepared for %s (binary '\''%s'\'' missing from sandbox prefix) — run %s --prepare first\n' \
526
+ "$v" "$(bin_for "$v")" "$0" >&2
527
+ exit 3
528
+ fi
529
+ prepare_project "$v"
530
+ build_battery "$v"
531
+ log "[$v] opencode mcp list"
532
+ check_mcp_list "$v"
533
+ log "[$v] mcp-list → ${RESULTS["$v:mcp-list"]}"
534
+ log "[$v] doctor"
535
+ check_doctor "$v"
536
+ log "[$v] doctor → ${RESULTS["$v:doctor"]}"
537
+ local fail=0
538
+ [ "${RESULTS["$v:mcp-list"]}" = "FAIL" ] && fail=$((fail + 1)) || true
539
+ [ "${RESULTS["$v:doctor"]}" = "FAIL" ] && fail=$((fail + 1)) || true
540
+ if [ "$fail" -gt 0 ]; then
541
+ log "Base validation $v: $fail real FAIL(s)."
542
+ exit 1
543
+ fi
544
+ log "Base validation $v: OK."
545
+ }
546
+
547
+ # ── CLI ───────────────────────────────────────────────────────────────────────
548
+
549
+ while [ $# -gt 0 ]; do
550
+ case "$1" in
551
+ --prepare) MODE_PREPARE=1 ;;
552
+ --reset) MODE_RESET=1 ;;
553
+ --prompts) MODE_PROMPTS=1 ;;
554
+ --run)
555
+ shift
556
+ case "${1:-}" in
557
+ v1 | v2) RUN_VERSION="$1" ;;
558
+ *) usage ;;
559
+ esac
560
+ ;;
561
+ --help | -h) usage ;;
562
+ *) usage ;;
563
+ esac
564
+ shift
565
+ done
566
+
567
+ if [ "$MODE_RESET" -eq 1 ]; then
568
+ cmd_reset
569
+ exit 0
570
+ fi
571
+
572
+ if [ "$MODE_PREPARE" -eq 0 ] && [ -z "$RUN_VERSION" ] && [ "$MODE_PROMPTS" -eq 0 ]; then
573
+ usage
574
+ fi
575
+
576
+ if [ "$MODE_PREPARE" -eq 1 ]; then
577
+ cmd_prepare
578
+ fi
579
+
580
+ if [ -n "$RUN_VERSION" ]; then
581
+ if [ "$MODE_PROMPTS" -eq 1 ]; then
582
+ sandbox_env
583
+ run_battery "$RUN_VERSION"
584
+ else
585
+ sandbox_env
586
+ run_base "$RUN_VERSION"
587
+ fi
588
+ elif [ "$MODE_PROMPTS" -eq 1 ]; then
589
+ sandbox_env
590
+ run_battery v1 v2
591
+ fi