pantheon-opencode 1.5.2 → 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 (60) 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 +17 -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/mcp_persistence_server.py +390 -76
  23. package/scripts/memory_mcp_server.py +370 -196
  24. package/scripts/postinstall.mjs +0 -7
  25. package/scripts/probe-context-rehydrate.mjs +28 -11
  26. package/scripts/prune.mjs +2 -2
  27. package/scripts/test-opencode-v2-sandbox.sh +1759 -0
  28. package/scripts/validate-routing.mjs +40 -0
  29. package/skills-lock.json +9 -10
  30. package/src/agents/mnemosyne.md +15 -16
  31. package/src/agents/zeus.md +1 -2
  32. package/src/mcp/code_mode_server.py +116 -24
  33. package/src/mcp/mcp_codemap_module.py +559 -129
  34. package/src/mcp/mcp_persistence_server.py +390 -76
  35. package/src/mcp/memory_mcp_server.py +371 -248
  36. package/src/mcp/pantheon_vision_server.py +136 -24
  37. package/src/mcp/requirements-mcp.txt +5 -3
  38. package/src/mcp/requirements-vision.txt +2 -0
  39. package/src/pantheon/model-command.ts +18 -7
  40. package/src/pantheon/todo-enforcer.ts +1 -1
  41. package/src/pantheon/vision.ts +1 -1
  42. package/src/plugin-v2.ts +134 -104
  43. package/src/plugins/tui/dist/tui.js +1 -1
  44. package/src/plugins/tui/package-lock.json +20 -20
  45. package/src/plugins/tui/package.json +12 -12
  46. package/src/routing.yml +0 -1
  47. package/src/skills/README.md +31 -36
  48. package/src/skills/auto-continue/SKILL.md +25 -75
  49. package/.pantheon/code-mode/eval-llm-judge.py +0 -255
  50. package/.pantheon/code-mode/eval-monte-carlo.py +0 -368
  51. package/.pantheon/code-mode/eval-run.py +0 -197
  52. package/.pantheon/code-mode/eval-static.py +0 -321
  53. package/.pantheon/code-mode/session-end-save.py +0 -186
  54. package/.pantheon/code-mode/session-end-save.sh +0 -18
  55. package/scripts/recover-zenodo-deposition.mjs +0 -146
  56. package/scripts/test-opencode-v1-v2-sandbox.sh +0 -862
  57. package/scripts/validate-zenodo-release.mjs +0 -154
  58. package/scripts/zenodo-artifact.mjs +0 -94
  59. package/scripts/zenodo-release-state.mjs +0 -53
  60. package/src/skills/orchestration-workflow/SKILL.md +0 -311
@@ -1,368 +0,0 @@
1
- #!/usr/bin/env python3
2
- # ---
3
- # description: Monte Carlo reliability scoring for skills/agents — simulates N runs of the core workflow (plugin-eval layer 3)
4
- # timeout: 120
5
- # ---
6
- """Monte Carlo reliability layer of the plugin-eval certification pipeline (PR 3).
7
-
8
- Simulates N runs of a skill/agent's core workflow. Each run re-verifies the
9
- frontmatter, a seeded random subset of referenced files/commands, and (when
10
- present) the skill's test/verify command. Reliability = passes / runs * 100.
11
-
12
- For agents, additionally verifies that `skills:` and `@agent` references point
13
- at real skills/agents and that the YAML frontmatter is valid.
14
-
15
- Emits a single JSON document on stdout:
16
- {runs, passes, reliability (0-100), failures: [{run, reasons}, ...], ...}
17
-
18
- Exit codes: 0 = reliability >= 75, 1 = reliability < 75, 2 = usage/IO error.
19
- The declared test command executes once per invocation and its result is
20
- reused across runs (it is deterministic; re-running it N times only burns time).
21
- """
22
-
23
- from __future__ import annotations
24
-
25
- import argparse
26
- import json
27
- import os
28
- import random
29
- import re
30
- import shlex
31
- import signal
32
- import subprocess
33
- import sys
34
- from datetime import date
35
- from pathlib import Path
36
- from typing import Any
37
-
38
- DEFAULT_RUNS = 20
39
- DEFAULT_SEED = 42
40
- TEST_TIMEOUT = 15.0
41
- SAFE_COMMAND_PREFIXES = ("pytest", "python -m pytest", "npm test", "npm run test", "npm run verify", "bash", "python", "python3", "./")
42
- KNOWN_AGENTS = frozenset(
43
- {
44
- "athena", "apollo", "hermes", "aphrodite", "demeter", "themis",
45
- "prometheus", "hephaestus", "nyx", "gaia", "iris", "mnemosyne",
46
- "talos", "zeus",
47
- }
48
- )
49
- FILE_REF_RE = re.compile(r"`([\w./-]+\.(?:py|sh|js|ts|mjs|md|json|yml|yaml|toml))`")
50
- COMMAND_STARTS = ("python", "python3", "bash", "npm", "node", "npx", "pytest", "uv", "./")
51
- SUPPORTED_COMMANDS = frozenset({"python", "python3", "bash", "npm", "node", "npx", "pytest", "uv"})
52
- SHELL_OPERATORS = (";", "&&", "||", "|", ">", "<", "`", "$(")
53
- TEST_SECTION_RE = re.compile(r"^##+\s+(?:Verification|Testing|Test)\b.*$", re.MULTILINE)
54
-
55
-
56
- def _coerce(value: str) -> Any:
57
- """Coerce a scalar string into bool/int/float when possible."""
58
- lowered = value.lower()
59
- if lowered in ("true", "false"):
60
- return lowered == "true"
61
- if lowered in ("null", "none", "~"):
62
- return None
63
- try:
64
- return int(value)
65
- except ValueError:
66
- pass
67
- try:
68
- return float(value)
69
- except ValueError:
70
- pass
71
- return value
72
-
73
-
74
- def _minimal_yaml(block: str) -> dict[str, Any]:
75
- """Tiny YAML-subset parser for flat frontmatter (key: value, lists)."""
76
- data: dict[str, Any] = {}
77
- current_key: str | None = None
78
- for raw in block.splitlines():
79
- line = raw.rstrip()
80
- if not line.strip() or line.lstrip().startswith("#"):
81
- continue
82
- if line.lstrip().startswith("- "):
83
- item = line.split("- ", 1)[1].strip().strip('"').strip("'")
84
- if current_key is not None:
85
- data.setdefault(current_key, []).append(item)
86
- continue
87
- if ":" not in line:
88
- continue
89
- key, _, value = line.partition(":")
90
- current_key = key.strip()
91
- value = value.strip()
92
- if not value:
93
- data[current_key] = []
94
- else:
95
- data[current_key] = _coerce(value.strip('"').strip("'"))
96
- return data
97
-
98
-
99
- def _parse_frontmatter(text: str) -> dict[str, Any] | None:
100
- """Parse a YAML frontmatter block; return None when absent or invalid."""
101
- if not text.startswith("---"):
102
- return None
103
- end = text.find("\n---", 3)
104
- if end == -1:
105
- return None
106
- block = text[3:end]
107
- try:
108
- import yaml # local import: optional dependency
109
-
110
- data = yaml.safe_load(block)
111
- return data if isinstance(data, dict) else None
112
- except ImportError:
113
- return _minimal_yaml(block)
114
- except Exception: # noqa: BLE001 - any YAML error means invalid frontmatter
115
- return None
116
-
117
-
118
- def _discover_doc(path: Path) -> Path:
119
- """Return the primary doc (SKILL.md or agent .md) for a path."""
120
- if path.is_file():
121
- return path
122
- skill = path / "SKILL.md"
123
- if skill.is_file():
124
- return skill
125
- candidates = sorted(path.glob("*.md"))
126
- for candidate in candidates:
127
- try:
128
- text = candidate.read_text(encoding="utf-8", errors="replace")
129
- except OSError:
130
- continue
131
- if _parse_frontmatter(text) is not None:
132
- return candidate
133
- if candidates:
134
- return candidates[0]
135
- raise ValueError(f"No SKILL.md or .md file found in {path}")
136
-
137
-
138
- def _as_list(value: Any) -> list[Any]:
139
- if isinstance(value, list):
140
- return value
141
- if isinstance(value, str) and value.strip():
142
- return [value]
143
- return []
144
-
145
-
146
- def _extract_references(doc: Path, text: str, is_agent: bool) -> dict[str, list[str]]:
147
- """Extract file refs, commands, agent mentions, and (agents) skill refs."""
148
- refs: dict[str, list[str]] = {"files": [], "commands": [], "agents": [], "skills": []}
149
- for match in FILE_REF_RE.finditer(text):
150
- ref = match.group(1)
151
- if ref not in refs["files"]:
152
- refs["files"].append(ref)
153
- for line in text.splitlines():
154
- stripped = line.strip()
155
- if not stripped.startswith(COMMAND_STARTS) or stripped.startswith(("#", "//")):
156
- continue
157
- cmd = stripped.split("&&")[0].strip()
158
- if cmd and cmd not in refs["commands"]:
159
- refs["commands"].append(cmd)
160
- for mention in re.findall(r"@([a-z][a-z0-9-]*)", text):
161
- if mention not in refs["agents"]:
162
- refs["agents"].append(mention)
163
- if is_agent:
164
- frontmatter = _parse_frontmatter(text) or {}
165
- refs["skills"] = [str(s) for s in _as_list(frontmatter.get("skills"))]
166
- return refs
167
-
168
-
169
- def _find_test_command(doc: Path, text: str) -> str | None:
170
- """Return a test/verify command from frontmatter or a Verification section."""
171
- frontmatter = _parse_frontmatter(text) or {}
172
- for key in ("test", "test_command", "verify_command", "check_command"):
173
- value = frontmatter.get(key)
174
- if isinstance(value, str) and value.strip():
175
- return value.strip()
176
- for match in TEST_SECTION_RE.finditer(text):
177
- section = text[match.end() :].split("\n##", 1)[0]
178
- for line in section.splitlines():
179
- stripped = line.strip()
180
- if stripped.startswith(SAFE_COMMAND_PREFIXES) and not stripped.startswith(("#", "//")):
181
- return stripped.split("&&")[0].strip()
182
- return None
183
-
184
-
185
- def _check_frontmatter(doc: Path, is_agent: bool) -> tuple[bool, str]:
186
- """Validate the doc's YAML frontmatter and required keys."""
187
- try:
188
- text = doc.read_text(encoding="utf-8", errors="replace")
189
- except OSError as exc:
190
- return False, f"cannot read {doc.name}: {exc}"
191
- frontmatter = _parse_frontmatter(text)
192
- if frontmatter is None:
193
- return False, "frontmatter missing or invalid YAML"
194
- required = ("name", "description", "mode") if is_agent else ("name", "description")
195
- missing = [key for key in required if key not in frontmatter]
196
- if missing:
197
- return False, f"frontmatter missing keys: {', '.join(missing)}"
198
- return True, ""
199
-
200
-
201
- def _resolve_file(ref: str, base: Path, repo_root: Path) -> Path | None:
202
- """Resolve a file reference against the doc dir, repo root, or CWD."""
203
- candidate = Path(ref)
204
- if candidate.is_absolute():
205
- return candidate if candidate.exists() else None
206
- for root in (base, repo_root, Path.cwd()):
207
- resolved = root / ref
208
- if resolved.exists():
209
- return resolved
210
- return None
211
-
212
-
213
- def _check_file(ref: str, base: Path, repo_root: Path) -> tuple[bool, str]:
214
- """Verify a referenced file exists (and is executable for shell scripts)."""
215
- resolved = _resolve_file(ref, base, repo_root)
216
- if resolved is None:
217
- return False, f"missing file: {ref}"
218
- if resolved.suffix == ".sh" and not os.access(resolved, os.X_OK):
219
- return False, f"not executable: {ref}"
220
- return True, ""
221
-
222
-
223
- def _check_command(cmd: str, base: Path, repo_root: Path) -> tuple[bool, str]:
224
- """Verify a command's file argument exists (module-only commands pass)."""
225
- for token in cmd.split()[1:]:
226
- if "/" in token or token.endswith((".py", ".sh", ".js", ".mjs")):
227
- if _resolve_file(token, base, repo_root) is None:
228
- return False, f"command references missing file: {token}"
229
- break
230
- return True, ""
231
-
232
-
233
- def _run_test_command(cmd: str, base: Path) -> tuple[bool, str]:
234
- """Run an allowlisted command without invoking a shell."""
235
- if any(operator in cmd for operator in SHELL_OPERATORS):
236
- return False, f"unsafe test command skipped: shell operator in {cmd}"
237
- try:
238
- argv = shlex.split(cmd)
239
- except ValueError as exc:
240
- return False, f"unsafe test command skipped: invalid command syntax: {exc}"
241
- if not argv:
242
- return False, "unsafe test command skipped: empty command"
243
- executable = Path(argv[0]).name if argv[0].startswith("./") else argv[0]
244
- if executable not in SUPPORTED_COMMANDS and not argv[0].startswith("./"):
245
- return False, f"unsafe test command skipped: unsupported command: {argv[0]}"
246
- try:
247
- proc = subprocess.Popen(
248
- argv,
249
- cwd=base,
250
- stdout=subprocess.PIPE,
251
- stderr=subprocess.PIPE,
252
- text=True,
253
- start_new_session=True,
254
- )
255
- try:
256
- _stdout, _stderr = proc.communicate(timeout=TEST_TIMEOUT)
257
- except subprocess.TimeoutExpired:
258
- os.killpg(proc.pid, signal.SIGKILL)
259
- proc.wait()
260
- return False, f"test command timed out: {cmd}"
261
- except subprocess.TimeoutExpired:
262
- return False, f"test command timed out: {cmd}"
263
- except OSError as exc:
264
- return False, f"test command failed to start: {exc}"
265
- if proc.returncode != 0:
266
- return False, f"test command exited {proc.returncode}: {cmd}"
267
- return True, ""
268
-
269
-
270
- def _simulate(
271
- rng: random.Random,
272
- doc: Path,
273
- is_agent: bool,
274
- refs: dict[str, list[str]],
275
- test_cmd: str | None,
276
- test_result: tuple[bool, str] | None,
277
- repo_root: Path,
278
- skills_dirs: list[Path],
279
- agents_dir: Path,
280
- ) -> tuple[list[str], tuple[bool, str] | None]:
281
- """Run one simulated execution; return (failure reasons, cached test result)."""
282
- failures: list[str] = []
283
- ok, reason = _check_frontmatter(doc, is_agent)
284
- if not ok:
285
- failures.append(reason)
286
- files = refs["files"]
287
- if files:
288
- k = rng.randint(max(1, len(files) // 2), len(files))
289
- for ref in rng.sample(files, k):
290
- ok, reason = _check_file(ref, doc.parent, repo_root)
291
- if not ok:
292
- failures.append(reason)
293
- commands = refs["commands"]
294
- if commands:
295
- k = rng.randint(max(1, len(commands) // 2), len(commands))
296
- for cmd in rng.sample(commands, k):
297
- ok, reason = _check_command(cmd, doc.parent, repo_root)
298
- if not ok:
299
- failures.append(reason)
300
- for agent in refs["agents"]:
301
- if agent in KNOWN_AGENTS and not (agents_dir / f"{agent}.md").exists():
302
- failures.append(f"unknown agent reference: @{agent}")
303
- for skill in refs["skills"]:
304
- if not any((directory / skill / "SKILL.md").exists() for directory in skills_dirs):
305
- failures.append(f"unknown skill reference: {skill}")
306
- if test_cmd:
307
- if test_result is None:
308
- test_result = _run_test_command(test_cmd, doc.parent)
309
- if not test_result[0]:
310
- failures.append(test_result[1])
311
- return failures, test_result
312
-
313
-
314
- def main(argv: list[str] | None = None) -> int:
315
- """CLI entry point: `python eval-monte-carlo.py <path> [--runs N]`."""
316
- parser = argparse.ArgumentParser(
317
- description="Monte Carlo reliability scoring (plugin-eval layer 3)."
318
- )
319
- parser.add_argument("path", help="Path to a skill/agent directory or doc file")
320
- parser.add_argument("--runs", type=int, default=DEFAULT_RUNS, help="Simulated runs (default 20)")
321
- parser.add_argument("--seed", type=int, default=DEFAULT_SEED, help="RNG seed (default 42)")
322
- args = parser.parse_args(argv)
323
- if args.runs < 1:
324
- print("eval-monte-carlo: --runs must be >= 1", file=sys.stderr)
325
- return 2
326
- repo_root = Path(__file__).resolve().parent.parent.parent
327
- agents_dir = repo_root / "src" / "agents"
328
- skills_dirs = [repo_root / ".opencode" / "skills", Path.home() / ".config" / "opencode" / "skills"]
329
- try:
330
- doc = _discover_doc(Path(args.path))
331
- except ValueError as exc:
332
- print(f"eval-monte-carlo: {exc}", file=sys.stderr)
333
- return 2
334
- text = doc.read_text(encoding="utf-8", errors="replace")
335
- frontmatter = _parse_frontmatter(text) or {}
336
- is_agent = "mode" in frontmatter
337
- refs = _extract_references(doc, text, is_agent)
338
- test_cmd = _find_test_command(doc, text)
339
- rng = random.Random(args.seed)
340
- failures_list: list[dict[str, Any]] = []
341
- test_result: tuple[bool, str] | None = None
342
- passes = 0
343
- for run in range(1, args.runs + 1):
344
- failures, test_result = _simulate(
345
- rng, doc, is_agent, refs, test_cmd, test_result, repo_root, skills_dirs, agents_dir
346
- )
347
- if failures:
348
- failures_list.append({"run": run, "reasons": failures})
349
- else:
350
- passes += 1
351
- reliability = round(100.0 * passes / args.runs, 1)
352
- report = {
353
- "name": frontmatter.get("name") or doc.stem,
354
- "date": date.today().isoformat(),
355
- "runs": args.runs,
356
- "passes": passes,
357
- "reliability": reliability,
358
- "seed": args.seed,
359
- "kind": "agent" if is_agent else "skill",
360
- "test_command": test_cmd,
361
- "failures": failures_list,
362
- }
363
- print(json.dumps(report, ensure_ascii=False, indent=2))
364
- return 0 if reliability >= 75.0 else 1
365
-
366
-
367
- if __name__ == "__main__":
368
- sys.exit(main())
@@ -1,197 +0,0 @@
1
- #!/usr/bin/env python3
2
- # ---
3
- # description: plugin-eval orchestrator — runs static + LLM judge + Monte Carlo layers and emits one report JSON
4
- # timeout: 300
5
- # ---
6
- """Orchestrator for the plugin-eval certification pipeline (PR 3).
7
-
8
- Runs the three layers in sequence (static → LLM judge → Monte Carlo) and
9
- aggregates them into a single report JSON:
10
-
11
- {name, date, static, llm_judge, monte_carlo, overall_score, verdict}
12
-
13
- Verdict thresholds: certified >= 75, needs_work 50-74, failed < 50. Layer
14
- scripts may exit non-zero to signal a below-threshold score while still
15
- printing a valid report; such layers are marked ``below_threshold`` and
16
- their scores count toward the overall. Layers that fail outright (e.g. no
17
- LLM available, invalid JSON) are recorded with an `error` field and
18
- excluded from the overall score; the report still emits.
19
- """
20
-
21
- from __future__ import annotations
22
-
23
- import argparse
24
- import importlib.util
25
- import json
26
- import os
27
- import subprocess
28
- import sys
29
- from datetime import date
30
- from pathlib import Path
31
- from typing import Any
32
-
33
- SCRIPT_DIR = Path(__file__).resolve().parent
34
- STATIC_SCRIPT = SCRIPT_DIR / "eval-static.py"
35
- JUDGE_SCRIPT = SCRIPT_DIR / "eval-llm-judge.py"
36
- MONTE_SCRIPT = SCRIPT_DIR / "eval-monte-carlo.py"
37
- LAYER_TIMEOUT = 240.0
38
- SCORE_KEYS = ("score", "overall", "overall_score", "reliability", "total")
39
-
40
-
41
- def _run_layer(script: Path, target: str, extra: list[str] | None = None) -> dict[str, Any]:
42
- """Run one layer script; return parsed JSON or an error dict.
43
-
44
- A non-zero exit is not fatal by itself: layer scripts print their report
45
- JSON before exiting non-zero to signal a below-threshold score (e.g.
46
- eval-monte-carlo.py exits 1 when reliability < 75). In that case the
47
- parsed report is returned with ``below_threshold`` set so its score
48
- still counts toward the overall verdict. Only when stdout holds no
49
- usable report does the layer degrade to an error dict.
50
- """
51
- cmd = [sys.executable, str(script), target, *(extra or [])]
52
- try:
53
- proc = subprocess.run(cmd, capture_output=True, text=True, timeout=LAYER_TIMEOUT, check=False)
54
- except subprocess.TimeoutExpired:
55
- return {"error": f"{script.name} timed out"}
56
- try:
57
- data = json.loads(proc.stdout)
58
- except json.JSONDecodeError as exc:
59
- if proc.returncode != 0:
60
- return {"error": proc.stderr.strip() or f"{script.name} exited {proc.returncode}"}
61
- return {"error": f"{script.name} returned invalid JSON: {exc}"}
62
- if not isinstance(data, dict):
63
- if proc.returncode != 0:
64
- return {"error": proc.stderr.strip() or f"{script.name} exited {proc.returncode}"}
65
- return {"error": f"{script.name} returned non-object JSON"}
66
- if proc.returncode != 0:
67
- data["below_threshold"] = True
68
- return data
69
-
70
-
71
- def _extract_score(data: dict[str, Any], keys: tuple[str, ...]) -> float | None:
72
- """Find the first numeric score under any key, recursing into nested dicts."""
73
- for key in keys:
74
- value = data.get(key)
75
- if isinstance(value, (int, float)) and not isinstance(value, bool):
76
- return float(value)
77
- for value in data.values():
78
- if isinstance(value, dict):
79
- found = _extract_score(value, keys)
80
- if found is not None:
81
- return found
82
- return None
83
-
84
-
85
- def _verdict(score: float) -> str:
86
- if score >= 75:
87
- return "certified"
88
- if score >= 50:
89
- return "needs_work"
90
- return "failed"
91
-
92
-
93
- def _doc_name(path: Path) -> str:
94
- """Best-effort name from the doc frontmatter, falling back to the stem."""
95
- if path.is_file():
96
- doc = path
97
- else:
98
- skill = path / "SKILL.md"
99
- doc = skill if skill.is_file() else path
100
- try:
101
- text = doc.read_text(encoding="utf-8", errors="replace")
102
- except OSError:
103
- return path.stem
104
- if text.startswith("---"):
105
- end = text.find("\n---", 3)
106
- if end != -1:
107
- for line in text[3:end].splitlines():
108
- if line.startswith("name:"):
109
- return line.split(":", 1)[1].strip().strip('"').strip("'")
110
- return path.stem
111
-
112
-
113
- def _persist_report(report: dict[str, Any]) -> None:
114
- """Best-effort publish of the final report to the ``plugin_eval`` namespace.
115
-
116
- Reuses ``src/mcp/eval_store.py`` (stdlib-only, designed to be loaded from
117
- .pantheon/code-mode/ scripts via importlib) so DB path resolution
118
- (PANTHEON_HOME → XDG → ~/.config/opencode), schema and dedup stay in one
119
- place. Never raises: failures print a hint to stderr — the JSON report on
120
- stdout remains the single source of truth. No-op under pytest so tests
121
- never touch the real memory DB.
122
- """
123
- if "PYTEST_CURRENT_TEST" in os.environ:
124
- return
125
- try:
126
- store_path = SCRIPT_DIR.parent.parent / "src" / "mcp" / "eval_store.py"
127
- spec = importlib.util.spec_from_file_location("eval_store", str(store_path))
128
- module = importlib.util.module_from_spec(spec)
129
- spec.loader.exec_module(module)
130
- result = module.store_eval(
131
- report["name"], report, int(report.get("overall_score", 0)), report["date"]
132
- )
133
- if "error" in result:
134
- print(f"eval_store: {result['error']}", file=sys.stderr)
135
- except Exception: # noqa: BLE001
136
- print("To publish: use eval_store.store_eval() or the pantheon-memory MCP",
137
- file=sys.stderr)
138
-
139
-
140
- def main(argv: list[str] | None = None) -> int:
141
- """CLI entry point: `python eval-run.py <path> [--runs N] [--skip-llm]`."""
142
- parser = argparse.ArgumentParser(
143
- description="plugin-eval orchestrator (static + LLM judge + Monte Carlo)."
144
- )
145
- parser.add_argument("path", help="Path to a skill/agent directory or doc file")
146
- parser.add_argument("--runs", type=int, default=20, help="Monte Carlo runs (default 20)")
147
- parser.add_argument("--skip-llm", action="store_true", help="Skip the LLM judge layer")
148
- parser.add_argument("--allow-external-llm", action="store_true", help="Permit the LLM judge to send skill content externally")
149
- args = parser.parse_args(argv)
150
-
151
- report: dict[str, Any] = {
152
- "name": _doc_name(Path(args.path)),
153
- "date": date.today().isoformat(),
154
- "static": {},
155
- "llm_judge": {},
156
- "monte_carlo": {},
157
- }
158
- scores: list[float] = []
159
-
160
- if STATIC_SCRIPT.exists():
161
- static = _run_layer(STATIC_SCRIPT, args.path)
162
- report["static"] = static
163
- score = _extract_score(static, SCORE_KEYS)
164
- if score is not None:
165
- scores.append(score)
166
- else:
167
- report["static"] = {"skipped": True, "reason": "eval-static.py not present yet"}
168
-
169
- if args.skip_llm:
170
- report["llm_judge"] = {"skipped": True}
171
- else:
172
- judge_extra = ["--allow-external-llm"] if args.allow_external_llm else []
173
- judge = _run_layer(JUDGE_SCRIPT, args.path, judge_extra)
174
- report["llm_judge"] = judge
175
- score = _extract_score(judge, ("overall", "score"))
176
- if score is not None:
177
- scores.append(score)
178
-
179
- monte = _run_layer(MONTE_SCRIPT, args.path, ["--runs", str(args.runs)])
180
- report["monte_carlo"] = monte
181
- score = _extract_score(monte, ("reliability", "score"))
182
- if score is not None:
183
- scores.append(score)
184
-
185
- overall = round(sum(scores) / len(scores), 1) if scores else 0.0
186
- report["overall_score"] = overall
187
- report["layers_scored"] = len(scores)
188
- if not scores:
189
- report["note"] = "no layer produced a score; overall_score is 0"
190
- report["verdict"] = _verdict(overall)
191
- print(json.dumps(report, ensure_ascii=False, indent=2))
192
- _persist_report(report)
193
- return 0
194
-
195
-
196
- if __name__ == "__main__":
197
- sys.exit(main())