pantheon-opencode 1.5.1 → 1.6.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 (62) hide show
  1. package/.pantheon/code-mode/README.md +4 -2
  2. package/.pantheon/code-mode/checkpoint_session.py +138 -102
  3. package/.pantheon/code-mode/compress-inline.py +102 -57
  4. package/.pantheon/code-mode/manifest.json +2 -8
  5. package/CHANGELOG.md +22 -0
  6. package/README.md +86 -52
  7. package/README.pt-BR.md +63 -43
  8. package/commands/pantheon-optimize.md +2 -2
  9. package/package.json +13 -1
  10. package/plugin.json +1 -1
  11. package/pyproject.toml +8 -15
  12. package/scripts/build-agents-md.mjs +2 -2
  13. package/scripts/check-plugin-v2-coverage.mjs +213 -0
  14. package/scripts/ci-validate-yaml.py +7 -5
  15. package/scripts/code_mode_server.py +116 -24
  16. package/scripts/doctor.mjs +85 -28
  17. package/scripts/install/cli-ui.mjs +1 -1
  18. package/scripts/install/config-migration.mjs +2 -2
  19. package/scripts/install/health-check.mjs +6 -19
  20. package/scripts/install/interactive.mjs +6 -6
  21. package/scripts/install/migrate.mjs +1 -1
  22. package/scripts/install/state.mjs +27 -2
  23. package/scripts/mcp_persistence_server.py +390 -76
  24. package/scripts/memory_mcp_server.py +370 -196
  25. package/scripts/postinstall.mjs +0 -7
  26. package/scripts/probe-context-rehydrate.mjs +28 -11
  27. package/scripts/prune.mjs +2 -2
  28. package/scripts/sync-tui.mjs +11 -5
  29. package/scripts/test-opencode-v2-sandbox.sh +1759 -0
  30. package/scripts/validate-routing.mjs +40 -0
  31. package/skills-lock.json +9 -10
  32. package/src/agents/mnemosyne.md +15 -16
  33. package/src/agents/zeus.md +1 -2
  34. package/src/mcp/code_mode_server.py +116 -24
  35. package/src/mcp/mcp_codemap_module.py +559 -129
  36. package/src/mcp/mcp_persistence_server.py +390 -76
  37. package/src/mcp/memory_mcp_server.py +371 -248
  38. package/src/mcp/pantheon_vision_server.py +136 -24
  39. package/src/mcp/requirements-mcp.txt +5 -3
  40. package/src/mcp/requirements-vision.txt +2 -0
  41. package/src/pantheon/model-command.ts +18 -7
  42. package/src/pantheon/todo-enforcer.ts +1 -1
  43. package/src/pantheon/vision.ts +1 -1
  44. package/src/plugin-v2.ts +134 -104
  45. package/src/plugins/tui/dist/tui.js +1 -1
  46. package/src/plugins/tui/package-lock.json +20 -20
  47. package/src/plugins/tui/package.json +12 -12
  48. package/src/routing.yml +0 -1
  49. package/src/skills/README.md +31 -36
  50. package/src/skills/auto-continue/SKILL.md +25 -75
  51. package/.pantheon/code-mode/eval-llm-judge.py +0 -255
  52. package/.pantheon/code-mode/eval-monte-carlo.py +0 -368
  53. package/.pantheon/code-mode/eval-run.py +0 -197
  54. package/.pantheon/code-mode/eval-static.py +0 -321
  55. package/.pantheon/code-mode/session-end-save.py +0 -186
  56. package/.pantheon/code-mode/session-end-save.sh +0 -18
  57. package/scripts/recover-zenodo-deposition.mjs +0 -146
  58. package/scripts/test-opencode-v1-v2-sandbox.sh +0 -862
  59. package/scripts/validate-zenodo-release.mjs +0 -154
  60. package/scripts/zenodo-artifact.mjs +0 -21
  61. package/scripts/zenodo-release-state.mjs +0 -53
  62. package/src/skills/orchestration-workflow/SKILL.md +0 -311
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+
2
3
  /**
3
4
  * validate-routing.mjs — Validate routing.yml consistency
4
5
  *
@@ -14,6 +15,7 @@
14
15
  * node scripts/validate-routing.mjs --verbose # show all entries checked
15
16
  */
16
17
 
18
+ import { createHash } from 'node:crypto'
17
19
  import { existsSync, readdirSync, readFileSync } from 'node:fs'
18
20
  import { dirname, join } from 'node:path'
19
21
  import { fileURLToPath } from 'node:url'
@@ -106,6 +108,44 @@ for (const [name, info] of Object.entries(routing.agents || {})) {
106
108
  }
107
109
  }
108
110
 
111
+ // B2. skills-lock.json must match src/skills/*/SKILL.md exactly — content
112
+ // digests stay in sync, and the set of locked paths equals the set on disk
113
+ // (so adding/removing a skill without updating the lock fails the gate).
114
+ console.log('\n skills-lock.json integrity:')
115
+ const lockPath = join(ROOT, 'skills-lock.json')
116
+ let skillLock = null
117
+ try {
118
+ skillLock = JSON.parse(readFileSync(lockPath, 'utf8'))
119
+ } catch (err) {
120
+ check(false, `skills-lock.json missing or malformed: ${err.message}`)
121
+ }
122
+ if (skillLock && typeof skillLock === 'object' && !Array.isArray(skillLock)) {
123
+ const lockedFiles = Object.keys(skillLock).sort()
124
+ const skillFiles = existsSync(srcSkillsDir)
125
+ ? readdirSync(srcSkillsDir)
126
+ .filter((d) => existsSync(join(srcSkillsDir, d, 'SKILL.md')))
127
+ .map((d) => `src/skills/${d}/SKILL.md`)
128
+ .sort()
129
+ : []
130
+ console.log(` Locked skills: ${lockedFiles.length} (on disk: ${skillFiles.length})`)
131
+
132
+ for (const rel of skillFiles) {
133
+ const actual = createHash('sha256')
134
+ .update(readFileSync(join(ROOT, rel)))
135
+ .digest('hex')
136
+ check(
137
+ skillLock[rel] === actual,
138
+ `skills-lock.json stale for "${rel}": expected ${actual}, got ${skillLock[rel] ?? 'no entry'}`,
139
+ )
140
+ }
141
+ for (const rel of lockedFiles) {
142
+ check(
143
+ skillFiles.includes(rel),
144
+ `skills-lock.json lists "${rel}" but that SKILL.md does not exist`,
145
+ )
146
+ }
147
+ }
148
+
109
149
  // C. Handoff validation
110
150
  console.log(
111
151
  `\n Handoff definitions: ${Object.keys(routing.handoffs || {}).length} agents with handoffs`,
package/skills-lock.json CHANGED
@@ -1,23 +1,22 @@
1
1
  {
2
2
  "src/skills/agent-coordination/SKILL.md": "cfd040ef29e3dd0fd5e0e67773874facf7c06e3284898afe1fddba4456c3f1aa",
3
3
  "src/skills/artifact-management/SKILL.md": "c60e493666bedf46e9113d0dd20106489d5c5a73905e1370fcfc1c0d71c59185",
4
- "src/skills/auto-continue/SKILL.md": "791b7f8d981bca571a6350cfd0f7f623ab8d9c2190b015dc63c15f8663b25e53",
4
+ "src/skills/auto-continue/SKILL.md": "f6ff530b5537af9c8a0224a52eb0da6da7f9cf4ab4958ce2908c5c3a911b8879",
5
+ "src/skills/clonedeps/SKILL.md": "3e0b43cf96da98c83699dc48db3fd58fbfda84b9022bb70d3747875c81c3e124",
5
6
  "src/skills/code-review-checklist/SKILL.md": "0e59367c65ef4608db80a0674279f32cfc3f67dca7236f233b61a47fe506579f",
6
- "src/skills/context-compression/SKILL.md": "07cfddfedeb8087794ae49ef8e60d8adca3ec2342d592cc30d0407d1b8ab1b90",
7
+ "src/skills/codemap/SKILL.md": "87af5271e1576e154cadff313eab16264b726e43d094520736b7a5b03b409a4a",
8
+ "src/skills/context-compression/SKILL.md": "ebc721c715c1fa20e4ebf5177007fde94e001540b16cef4316393a665633c16f",
7
9
  "src/skills/git-workflow-and-versioning/SKILL.md": "685a94041f1b4ce2a430301f517b89102055696267c369a7e8fb191606659276",
8
10
  "src/skills/incremental-implementation/SKILL.md": "7065ddb45da3c6b6751de07f290e92e6cb072e949a2e3aaae13ee7762f0a70fc",
11
+ "src/skills/loop-engineering/SKILL.md": "f804e545821b9cca8fe0ec517307ed902446f28c2ca8ff9194c0f987cf7229e3",
9
12
  "src/skills/memory-bank/SKILL.md": "4717cf18b43550c9f02cfab810968b97a5b2584f00a15516ecba643c80cdc110",
10
- "src/skills/orchestration-workflow/SKILL.md": "0eeabaf71a802d1f673f79996548cda2d394bbcc7c62eaca03482d3fa99be688",
13
+ "src/skills/reflect/SKILL.md": "4c7608c2a55cb8a50ac90fc0e2446bdf25e7914b1748cb7731c311d13c29dfdd",
11
14
  "src/skills/security-hardening/SKILL.md": "6d637e122421f7bc3b751f04854ad343a5a5d35249458bc8939233138fd3f20d",
12
15
  "src/skills/session-goal/SKILL.md": "793498265f70253c20524f6adc88bcc86db11d7160fe8232a26aedc6b1c5489f",
16
+ "src/skills/simplify/SKILL.md": "aa739e9f75962a77d6574557d674ea01b5aea95e580b721d66c058753161b053",
13
17
  "src/skills/spec-driven-development/SKILL.md": "e93b9103d6cd1848ab01328efde82c1daf564d1cabbdc2278254b346fcf55d02",
14
18
  "src/skills/tdd-with-agents/SKILL.md": "9880919a46b74f549f948147d8b086b008635de94fa259f76261d067772bb8e3",
19
+ "src/skills/verification-planning/SKILL.md": "14186446ef51e1ba2c87b337e8d5f86e566d805156ab21fba2a75f8c4a99df3f",
15
20
  "src/skills/visual-review-pipeline/SKILL.md": "f6e65ff75a9325c38e0e9dde8e87f09b873e14e490936ef48aa80201bb68fe10",
16
- "src/skills/codemap/SKILL.md": "src/skills/codemap/SKILL.md",
17
- "src/skills/simplify/SKILL.md": "src/skills/simplify/SKILL.md",
18
- "src/skills/reflect/SKILL.md": "src/skills/reflect/SKILL.md",
19
- "src/skills/worktrees/SKILL.md": "src/skills/worktrees/SKILL.md",
20
- "src/skills/verification-planning/SKILL.md": "src/skills/verification-planning/SKILL.md",
21
- "src/skills/loop-engineering/SKILL.md": "src/skills/loop-engineering/SKILL.md",
22
- "src/skills/clonedeps/SKILL.md": "src/skills/clonedeps/SKILL.md"
21
+ "src/skills/worktrees/SKILL.md": "0033c5f8a50f210f31f0256ba8cc0fd99c65674a2aabedb05d8e69447b5ec218"
23
22
  }
@@ -114,7 +114,7 @@ Mnemosyne executes the expanded compression pipeline. When Zeus delegates compre
114
114
  - Append new entries to `_xref/index.md`
115
115
  - Increment `_xref/_next_id.json`
116
116
 
117
- 7. **Auto-index vector memory**: Run `scripts/vector_memory/index.index_all()` to index new entries into the Level 3 Vector Memory system. If sentence-transformers is not installed, indexes FTS5 only.
117
+ 7. **Auto-index memory**: Store the new entries with the `memory_store` MCP tool (namespace `memory-bank`) so `memory_search` can find them. Search is FTS5 (BM25) keyword retrieval — there is no embedding step to run or report on.
118
118
 
119
119
  8. **Report**: Return summary: "Compressed. 2 CRITICAL, 1 HIGH, 3 STANDARD. Budget: 15/20 lines. Cross-refs: +2 entities, +1 decision. Indexed X new, skipped Y duplicates."
120
120
 
@@ -127,16 +127,16 @@ Mnemosyne executes the expanded compression pipeline. When Zeus delegates compre
127
127
  - NEVER write over existing entries (idempotency by date+phase+agent hash)
128
128
  - NEVER delete _xref/ entries (append-only)
129
129
 
130
- ## Semantic Recall Handler (Level 3)
130
+ ## Recall Handler (Level 3)
131
131
 
132
- Mnemosyne provides semantic recall via the Level 3 Vector Memory system:
132
+ Mnemosyne recalls stored entries with the `memory_search` MCP tool:
133
133
 
134
134
  **Command:** `@mnemosyne Recall "<query>" [--top-k 5] [--type adr|subtask|wisdom|impl|decision] [--agent hermes] [--since 2026-01-01] [--tags auth,jwt]`
135
135
 
136
136
  **How it works:**
137
- 1. Calls `scripts/vector_memory/query.recall()` with the provided parameters
138
- 2. Returns ranked, structured results with scores and source paths
139
- 3. Uses fallback chain: vector KNN → FTS5 BM25 → flat grep
137
+ 1. Calls `memory_search` with the query, namespace, and `top_k`
138
+ 2. Returns ranked, structured results with BM25 scores and source keys
139
+ 3. Retrieval is lexical: stopwords are dropped and terms of 4+ characters are prefix-matched, so queries should use words that actually appear in the stored text. Filter by type/agent/tags in the caller — `memory_search` filters on `namespace` only.
140
140
 
141
141
  **Usage examples:**
142
142
  ```
@@ -146,14 +146,13 @@ Mnemosyne provides semantic recall via the Level 3 Vector Memory system:
146
146
  ```
147
147
 
148
148
  **Integration with compress_context:**
149
- After each `compress_context` run, automatically index new entries:
150
- 1. Run `scripts/vector_memory/index.index_all()`
149
+ After each `compress_context` run, store the new entries with `memory_store`:
150
+ 1. Store each new entry with namespace `memory-bank` and a `memory-bank/<id>` key
151
151
  2. Report: "Indexed X new memories, skipped Y duplicates"
152
- 3. If sentence-transformers is not installed, skip vector indexing but still index FTS5
153
152
 
154
153
  **Integration with Close sprint:**
155
154
  When `Close sprint` is called, before wiping .tmp/:
156
- 1. Run final batch: `index_all()`
155
+ 1. Store the final batch of entries
157
156
  2. Report final index stats
158
157
 
159
158
  ## Invocation Rules
@@ -164,8 +163,8 @@ When `Close sprint` is called, before wiping .tmp/:
164
163
  ## Quick-Index Handler (Tier 1 — Background Agent Results)
165
164
 
166
165
  Called automatically by Zeus when any agent returns a subtask_summary
167
- (background or foreground). Persists results into Vector Memory immediately,
168
- no Themis needed.
166
+ (background or foreground). Persists results via the `memory_store` MCP tool
167
+ immediately, no Themis needed.
169
168
 
170
169
  **Trigger patterns:**
171
170
  - Background agent completes → Zeus calls Mnemosyne Quick-index
@@ -175,9 +174,9 @@ no Themis needed.
175
174
  **Command:** `@mnemosyne Quick-index <subtask_summary_json>`
176
175
 
177
176
  **What it does:**
178
- 1. Calls `scripts/vector_memory/index.quick_index()` with the summary dict
179
- 2. Auto-generates tags from keywords (no manual specification needed)
180
- 3. Reports: "Indexed: {type} from @{agent} ({memory_id})"
177
+ 1. Calls `memory_store` with the summary fields
178
+ 2. Stores under a `type/agent` namespace and a `<type>:<agent>:<date>` key
179
+ 3. Reports: "Indexed: {type} from @{agent} (namespace {namespace})"
181
180
 
182
181
  **Parameters (from subtask_summary):**
183
182
  | Field | Source | Required |
@@ -194,7 +193,7 @@ no Themis needed.
194
193
  **Safety:**
195
194
  - [OK] Safe to call on partial results (indexes what's available)
196
195
  - [OK] Safe to call multiple times (idempotent)
197
- - [OK] Works without sentence-transformers (FTS5 only)
196
+ - [OK] Works with no embedding backend (FTS5 keyword search only)
198
197
  - [FAIL] Does NOT generate ZZ artifact (that's Tier 2)
199
198
  - [FAIL] Does NOT update 01-active-context.md (that's Tier 2)
200
199
 
@@ -26,7 +26,6 @@ skills:
26
26
  - artifact-management
27
27
  - context-compression
28
28
  - auto-continue
29
- - orchestration-workflow
30
29
  - incremental-implementation
31
30
 
32
31
  ---
@@ -202,7 +201,7 @@ Zeus (nivel 0) -> Apollo/Hermes (nivel 1) -> sub-subagente (nivel 2 max).
202
201
 
203
202
  | Tier | Trigger | Action |
204
203
  |------|---------|--------|
205
- | Tier 1 — Auto-index | Any agent returns subtask_summary | `memory_store()` direto -> Vector Memory |
204
+ | Tier 1 — Auto-index | Any agent returns subtask_summary | `memory_store()` direto -> FTS5 memory |
206
205
  | Tier 2 — Compression | Themis APPROVED | compress_context -> ZZ -> memory-bank |
207
206
 
208
207
  ## MCP Tools
@@ -20,6 +20,11 @@ import hashlib
20
20
  import json
21
21
  import logging
22
22
  import os
23
+
24
+ try:
25
+ import resource
26
+ except (ImportError, OSError, ValueError): # pragma: no cover - platform-specific
27
+ resource = None # type: ignore[assignment]
23
28
  import shutil
24
29
  import signal
25
30
  import stat
@@ -125,6 +130,88 @@ def _build_script_env(
125
130
  return env
126
131
 
127
132
 
133
+ # Additional tasks a sandboxed script may create beyond the UID's current
134
+ # count. RLIMIT_NPROC is a per-real-UID limit on the *total* number of
135
+ # processes AND threads, so the sandbox floor must clear everything the host
136
+ # already owns before a script can fork at all. This headroom is the actual
137
+ # anti-fork-bomb budget: the script still cannot exceed current + headroom.
138
+ NPROC_HEADROOM: int = 64
139
+
140
+
141
+ def _uid_task_count() -> int | None:
142
+ """Count the tasks (processes + threads) this real UID already owns.
143
+
144
+ The kernel compares the post-fork total against RLIMIT_NPROC, so this is
145
+ the number the sandbox limit has to exceed. Returns *None* when the count
146
+ cannot be established (no readable ``/proc``), which makes the caller
147
+ fail open on ``--nproc`` rather than pin a value that may be too low and
148
+ break every script.
149
+
150
+ The tally is one too high per process: ``/proc/<pid>/task`` already lists
151
+ the thread-group leader, so the extra ``1`` counts it twice. That bias
152
+ only widens the derived limit (the fail-open direction), so it is left in
153
+ place deliberately.
154
+ """
155
+ uid = os.getuid()
156
+ count = 0
157
+ try:
158
+ pids = os.listdir("/proc")
159
+ except OSError:
160
+ return None
161
+ for pid in pids:
162
+ if not pid.isdigit():
163
+ continue
164
+ proc_dir = f"/proc/{pid}"
165
+ try:
166
+ if os.stat(proc_dir).st_uid != uid:
167
+ continue
168
+ # ``/proc/<pid>/task`` already includes the thread-group leader,
169
+ # so the extra 1 counts it twice (over-count by one per process).
170
+ count += 1 + len(os.listdir(f"{proc_dir}/task"))
171
+ except OSError:
172
+ # Process exited between listdir and stat, or is not ours to read.
173
+ continue
174
+ return count
175
+
176
+
177
+ def _sandbox_nproc() -> str | None:
178
+ """Return the ``--nproc`` value for a sandboxed script, or *None*.
179
+
180
+ Derived from the host rather than hardcoded: the value is the UID's
181
+ current task count plus ``NPROC_HEADROOM``, clamped to the hard limit
182
+ (``prlimit`` fails with EPERM above it, and an unprivileged process cannot
183
+ raise its soft limit past the hard one).
184
+
185
+ Returns *None* when the count or the limit cannot be read, in which case
186
+ the caller omits ``--nproc``. ``--as`` and ``--cpu`` still apply, so the
187
+ sandbox keeps its memory and CPU boundaries.
188
+ """
189
+ current = _uid_task_count()
190
+ if current is None:
191
+ _log.warning(
192
+ "code-mode: cannot count this UID's tasks; omitting --nproc, so "
193
+ "the sandboxed script's fork budget is unbounded"
194
+ )
195
+ return None
196
+ if resource is None:
197
+ _log.warning(
198
+ "code-mode: cannot read RLIMIT_NPROC; omitting --nproc, so the "
199
+ "sandboxed script's fork budget is unbounded"
200
+ )
201
+ return None
202
+ try:
203
+ _soft, hard = resource.getrlimit(resource.RLIMIT_NPROC)
204
+ except (OSError, ValueError):
205
+ _log.warning(
206
+ "code-mode: cannot read RLIMIT_NPROC; omitting --nproc, so the "
207
+ "sandboxed script's fork budget is unbounded"
208
+ )
209
+ return None
210
+ if hard == getattr(resource, "RLIM_INFINITY", -1):
211
+ return str(current + NPROC_HEADROOM)
212
+ return str(max(1, min(current + NPROC_HEADROOM, hard)))
213
+
214
+
128
215
  def _prlimit_prefix(
129
216
  prlimit_path: str | None,
130
217
  timeout_s: int = SCRIPT_TIMEOUT,
@@ -132,21 +219,34 @@ def _prlimit_prefix(
132
219
  """Return a ``prlimit`` command prefix to wrap a subprocess, or *None*.
133
220
 
134
221
  On Linux with ``prlimit`` available the prefix adds:
135
- --nproc=512 --as=1073741824 --cpu=<timeout_s + 5>
136
-
137
- On non-Linux or when *prlimit_path* is falsy, returns *None* (fail-open).
222
+ --nproc=<uid task count + NPROC_HEADROOM> --as=1073741824
223
+ --cpu=<timeout_s + 5>
224
+
225
+ ``--nproc`` is derived per invocation by :func:`_sandbox_nproc` rather
226
+ than hardcoded. RLIMIT_NPROC is a per-real-UID total across processes AND
227
+ threads, so a fixed value below the host's current usage makes the very
228
+ first ``fork()`` inside the sandbox fail with EAGAIN — which is exactly
229
+ what a hardcoded ``--nproc=512`` did on a host already running more than
230
+ 512 tasks. The isolation property is unchanged: a script still cannot
231
+ create more than ``NPROC_HEADROOM`` additional tasks.
232
+
233
+ On non-Linux, when *prlimit_path* is falsy, or when the nproc limit cannot
234
+ be derived, the flag is omitted (fail-open); ``--as``/``--cpu`` remain.
138
235
  """
139
236
  if not prlimit_path:
140
237
  return None
141
238
 
142
239
  cpu_limit = timeout_s + 5
143
- return [
144
- prlimit_path,
145
- "--nproc=512",
240
+ prefix = [prlimit_path]
241
+ nproc = _sandbox_nproc()
242
+ if nproc is not None:
243
+ prefix.append(f"--nproc={nproc}")
244
+ prefix += [
146
245
  "--as=1073741824", # 1 GiB in bytes
147
246
  f"--cpu={cpu_limit}",
148
247
  "--",
149
248
  ]
249
+ return prefix
150
250
 
151
251
 
152
252
  def _detect_prlimit() -> str | None:
@@ -226,7 +326,9 @@ def _packaged_scripts_dir() -> Path | None:
226
326
  return None
227
327
 
228
328
 
229
- def _resolve_scripts_dir(candidates: list[Path], *, allow_packaged: bool = True) -> Path | None:
329
+ def _resolve_scripts_dir(
330
+ candidates: list[Path], *, allow_packaged: bool = True
331
+ ) -> Path | None:
230
332
  """Select the first usable candidate, optionally trying the package copy."""
231
333
  for candidate in candidates:
232
334
  if _has_usable_scripts(candidate):
@@ -377,9 +479,7 @@ def _load_manifest(scripts_dir: Path | None = None) -> dict[str, str]:
377
479
  ) from None
378
480
 
379
481
  if not isinstance(data, dict):
380
- raise ManifestError(
381
- "CORRUPT_DATA", "Code-mode manifest must be a JSON object."
382
- )
482
+ raise ManifestError("CORRUPT_DATA", "Code-mode manifest must be a JSON object.")
383
483
 
384
484
  if data.get("version") != 1:
385
485
  raise ManifestError(
@@ -527,7 +627,6 @@ def _contract_result(
527
627
  return f"[{status}] {message}"
528
628
 
529
629
 
530
-
531
630
  def _format_output(
532
631
  stdout: str,
533
632
  stderr: str,
@@ -818,22 +917,15 @@ async def execute_code_script(
818
917
  timeout_s=timeout_s,
819
918
  status="TIMEOUT",
820
919
  )
821
- except FileNotFoundError:
822
- return _build_result(
823
- "",
824
- f"Script '{script_name}' not found or interpreter missing.",
825
- -1,
826
- timed_out=False,
827
- duration_ms=0,
828
- metadata=metadata,
829
- json_output=json_output,
830
- timeout_s=timeout_s,
831
- status="UNAVAILABLE",
832
- )
833
920
  except OSError as e:
921
+ error = (
922
+ f"Script '{script_name}' not found or interpreter missing."
923
+ if isinstance(e, FileNotFoundError)
924
+ else f"Failed to execute script: {e}"
925
+ )
834
926
  return _build_result(
835
927
  "",
836
- f"Failed to execute script: {e}",
928
+ error,
837
929
  -1,
838
930
  timed_out=False,
839
931
  duration_ms=0,