chill-agent 1.4.0 → 1.6.0

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 (240) hide show
  1. package/.agents/rules/ponytail.md +30 -0
  2. package/.claude-plugin/contrast-mcp-launcher.mjs +31 -0
  3. package/.claude-plugin/marketplace.json +17 -0
  4. package/.claude-plugin/plugin.json +8 -3
  5. package/.cline-plugin/plugin.js +6 -0
  6. package/.clinerules/ponytail.md +30 -0
  7. package/.codex-plugin/plugin.json +4 -3
  8. package/.cursor/rules/ponytail.mdc +36 -0
  9. package/.cursor-plugin/marketplace.json +16 -0
  10. package/.cursor-plugin/plugin.json +2 -2
  11. package/.devin-plugin/plugin.json +2 -2
  12. package/.grok-plugin/marketplace.json +16 -0
  13. package/.kimi-plugin/plugin.json +11 -29
  14. package/.kiro/steering/ponytail.md +35 -0
  15. package/.omp-plugin/marketplace.json +15 -0
  16. package/.openclaw/skills/ponytail/SKILL.md +47 -0
  17. package/.openclaw/skills/ponytail-audit/SKILL.md +100 -0
  18. package/.openclaw/skills/ponytail-debt/SKILL.md +43 -0
  19. package/.openclaw/skills/ponytail-gain/SKILL.md +51 -0
  20. package/.openclaw/skills/ponytail-help/SKILL.md +76 -0
  21. package/.openclaw/skills/ponytail-review/SKILL.md +98 -0
  22. package/.opencode/command/ponytail-audit.md +5 -0
  23. package/.opencode/command/ponytail-debt.md +5 -0
  24. package/.opencode/command/ponytail-gain.md +5 -0
  25. package/.opencode/command/ponytail-help.md +5 -0
  26. package/.opencode/command/ponytail-review.md +5 -0
  27. package/.opencode/command/ponytail.md +5 -0
  28. package/.opencode/plugins/index.js +4 -0
  29. package/.opencode/plugins/ponytail-frontmatter.cjs +63 -0
  30. package/.opencode/plugins/ponytail.mjs +206 -0
  31. package/.qoder-plugin/plugin.json +12 -26
  32. package/.windsurf/rules/ponytail.md +30 -0
  33. package/AGENTS.md +32 -0
  34. package/__init__.py +228 -0
  35. package/assets/antislop-banner.png +0 -0
  36. package/assets/band/band.png +0 -0
  37. package/assets/band/board.png +0 -0
  38. package/assets/benchmark-3model.svg +21 -0
  39. package/assets/benchmark-agentic.svg +62 -0
  40. package/assets/codex-icon.png +0 -0
  41. package/assets/compare/code/after.webp +0 -0
  42. package/assets/compare/code/before.webp +0 -0
  43. package/assets/compare/text/after.webp +0 -0
  44. package/assets/compare/text/before.webp +0 -0
  45. package/assets/compare/ui/w-all.webp +0 -0
  46. package/assets/compare/ui/w-antislop.webp +0 -0
  47. package/assets/compare/ui/w-design.webp +0 -0
  48. package/assets/compare/ui/without.webp +0 -0
  49. package/assets/logo-dark.png +0 -0
  50. package/assets/logo-dark.svg +115 -0
  51. package/assets/logo-greenpt-dark.svg +27 -0
  52. package/assets/logo-greenpt.svg +27 -0
  53. package/assets/logo.png +0 -0
  54. package/assets/photoreunion-logo-dark.svg +4 -0
  55. package/assets/photoreunion-logo-light.svg +4 -0
  56. package/assets/profile.png +0 -0
  57. package/assets/retriever-logo-dark.svg +4 -0
  58. package/assets/retriever-logo-light.svg +4 -0
  59. package/assets/stories-banner.png +0 -0
  60. package/assets/v5/audit-es.png +0 -0
  61. package/assets/v5/audit-ja.png +0 -0
  62. package/assets/v5/audit-ko.png +0 -0
  63. package/assets/v5/audit-zh-CN.png +0 -0
  64. package/assets/v5/audit.png +0 -0
  65. package/assets/v5/beforeafter-es.png +0 -0
  66. package/assets/v5/beforeafter-ja.png +0 -0
  67. package/assets/v5/beforeafter-ko.png +0 -0
  68. package/assets/v5/beforeafter-zh-CN.png +0 -0
  69. package/assets/v5/beforeafter.png +0 -0
  70. package/assets/v5/chart-es.png +0 -0
  71. package/assets/v5/chart-ja.png +0 -0
  72. package/assets/v5/chart-ko.png +0 -0
  73. package/assets/v5/chart-zh-CN.png +0 -0
  74. package/assets/v5/chart.png +0 -0
  75. package/assets/v5/hero-es.jpg +0 -0
  76. package/assets/v5/hero-ja.jpg +0 -0
  77. package/assets/v5/hero-ko.jpg +0 -0
  78. package/assets/v5/hero-zh-CN.jpg +0 -0
  79. package/assets/v5/hero.jpg +0 -0
  80. package/assets/v5/ladder-es.png +0 -0
  81. package/assets/v5/ladder-ja.png +0 -0
  82. package/assets/v5/ladder-ko.png +0 -0
  83. package/assets/v5/ladder-zh-CN.png +0 -0
  84. package/assets/v5/ladder.png +0 -0
  85. package/assets/v5/review-es.png +0 -0
  86. package/assets/v5/review-ja.png +0 -0
  87. package/assets/v5/review-ko.png +0 -0
  88. package/assets/v5/review-zh-CN.png +0 -0
  89. package/assets/v5/review.png +0 -0
  90. package/assets/v5/tests-es.png +0 -0
  91. package/assets/v5/tests-ja.png +0 -0
  92. package/assets/v5/tests-ko.png +0 -0
  93. package/assets/v5/tests-zh-CN.png +0 -0
  94. package/assets/v5/tests.png +0 -0
  95. package/assets/waitlist-banner-es.png +0 -0
  96. package/assets/waitlist-banner-ja.png +0 -0
  97. package/assets/waitlist-banner-ko.png +0 -0
  98. package/assets/waitlist-banner-zh-CN.png +0 -0
  99. package/assets/waitlist-banner.png +0 -0
  100. package/benchmarks/.env.example +2 -0
  101. package/benchmarks/agentic/README.md +201 -0
  102. package/benchmarks/agentic/answer.py +288 -0
  103. package/benchmarks/agentic/compare.py +93 -0
  104. package/benchmarks/agentic/complete.py +178 -0
  105. package/benchmarks/agentic/judge.py +198 -0
  106. package/benchmarks/agentic/run.py +543 -0
  107. package/benchmarks/agentic/tasks.py +1544 -0
  108. package/benchmarks/arms/baseline.js +2 -0
  109. package/benchmarks/arms/caveman-SKILL.md +67 -0
  110. package/benchmarks/arms/caveman.js +8 -0
  111. package/benchmarks/arms/ponytail.js +8 -0
  112. package/benchmarks/behavior.js +54 -0
  113. package/benchmarks/behavior.yaml +40 -0
  114. package/benchmarks/correctness.js +291 -0
  115. package/benchmarks/loc.js +17 -19
  116. package/benchmarks/promptfooconfig.gemini.yaml +32 -0
  117. package/benchmarks/promptfooconfig.gpt-newest.yaml +33 -0
  118. package/benchmarks/promptfooconfig.yaml +41 -0
  119. package/benchmarks/results/2026-06-12-caveman-vs-ponytail.md +71 -0
  120. package/benchmarks/results/2026-06-12-v4-hardening-vs-caveman.md +125 -0
  121. package/benchmarks/results/2026-06-15-llama3.2-local.md +82 -0
  122. package/benchmarks/results/2026-06-16-correctness-gate-fix.md +113 -0
  123. package/benchmarks/results/2026-06-16-robustness-audit.md +135 -0
  124. package/benchmarks/results/2026-06-17-agentic-safety.md +164 -0
  125. package/benchmarks/results/2026-06-17-cost-verification.md +93 -0
  126. package/benchmarks/results/2026-06-18-agentic.md +219 -0
  127. package/benchmarks/results/2026-06-22-issue-245-217-comprehension.md +98 -0
  128. package/benchmarks/results/2026-10-07-agentic.md +186 -0
  129. package/cli/index.mjs +241 -0
  130. package/cli/lib/banner.mjs +21 -0
  131. package/cli/lib/install.mjs +498 -0
  132. package/cli/lib/settings.mjs +48 -0
  133. package/cli/package-lock.json +87 -0
  134. package/cli/package.json +42 -0
  135. package/cli/scripts/smoke-test.mjs +95 -0
  136. package/cli/scripts/smoke-worker.mjs +411 -0
  137. package/cli/scripts/sync-skills.mjs +81 -0
  138. package/commands/ponytail-audit.toml +2 -0
  139. package/commands/ponytail-debt.toml +2 -0
  140. package/commands/ponytail-gain.toml +2 -0
  141. package/commands/ponytail-help.toml +2 -0
  142. package/commands/ponytail-review.toml +2 -0
  143. package/commands/ponytail.toml +2 -0
  144. package/examples/email-validation.md +156 -0
  145. package/examples/group-by.md +35 -0
  146. package/examples/infinite-scroll.md +58 -0
  147. package/examples/number-formatting.md +37 -0
  148. package/examples/rate-limit.md +272 -0
  149. package/examples/react-countdown.md +390 -0
  150. package/examples/url-params.md +41 -0
  151. package/gemini-extension.json +2 -2
  152. package/hooks/band/board.ts +560 -0
  153. package/hooks/band/logo.ts +3 -0
  154. package/hooks/band/metrics.ts +311 -0
  155. package/hooks/band/register.tsx +495 -0
  156. package/hooks/band/types.d.ts +34 -0
  157. package/hooks/chill-statusline.ps1 +1 -1
  158. package/hooks/claude-band.json +6 -0
  159. package/hooks/claude-codex-hooks.json +34 -4
  160. package/hooks/copilot-hooks.json +12 -2
  161. package/hooks/cursor-hooks.json +2 -2
  162. package/hooks/ponytail-activate.js +201 -0
  163. package/hooks/ponytail-band-import.js +274 -0
  164. package/hooks/ponytail-config.js +175 -0
  165. package/hooks/ponytail-instructions.js +98 -0
  166. package/hooks/ponytail-map.js +111 -0
  167. package/hooks/ponytail-mode-tracker.js +216 -0
  168. package/hooks/ponytail-runtime.js +182 -0
  169. package/hooks/ponytail-statusline.ps1 +24 -0
  170. package/hooks/ponytail-statusline.sh +18 -0
  171. package/hooks/ponytail-subagent.js +94 -0
  172. package/hooks/qoder-hooks.json +19 -4
  173. package/i18n/README.ko.md +230 -0
  174. package/package.json +17 -6
  175. package/pi-extension/index.js +254 -0
  176. package/pi-extension/package.json +8 -0
  177. package/pi-extension/test/extension.test.js +367 -0
  178. package/pi-extension/test/helpers.test.js +179 -0
  179. package/plugin.yaml +21 -12
  180. package/rules/antislop.mdc +18 -0
  181. package/scripts/build-openclaw-skills.js +60 -0
  182. package/scripts/check-repo.mjs +389 -0
  183. package/scripts/check-rule-copies.js +74 -0
  184. package/scripts/check-versions.js +77 -0
  185. package/scripts/cursor-hooks.js +145 -0
  186. package/scripts/generate-chillslop.mjs +42 -0
  187. package/scripts/generate-examples.mjs +149 -0
  188. package/scripts/generate-plugin-doors.mjs +108 -0
  189. package/scripts/publish-openclaw-skills.js +75 -0
  190. package/scripts/skill-index.mjs +81 -0
  191. package/scripts/sync-ponytail.mjs +65 -0
  192. package/scripts/uninstall.js +92 -0
  193. package/skills/antislop/SKILL.md +700 -0
  194. package/skills/antislop/VERSION +1 -0
  195. package/skills/antislop-code/SKILL.md +128 -0
  196. package/skills/antislop-copywriting/SKILL.md +407 -0
  197. package/skills/antislop-human/LICENSE-web-quality-skills +21 -0
  198. package/skills/antislop-human/SKILL.md +204 -0
  199. package/skills/antislop-human/contrast-check.py +168 -0
  200. package/skills/antislop-human/contrast-mcp.py +121 -0
  201. package/skills/antislop-human/references/A11Y-PATTERNS.md +249 -0
  202. package/skills/antislop-human/references/WCAG.md +127 -0
  203. package/skills/antislop-layoutmobile/SKILL.md +170 -0
  204. package/skills/antislop-ui/LICENSE-web-quality-skills +21 -0
  205. package/skills/antislop-ui/SKILL.md +344 -0
  206. package/skills/antislop-ui/references/CORE-WEB-VITALS.md +181 -0
  207. package/skills/antislop-ui/references/WEB-BASELINE.md +28 -0
  208. package/skills/plugin.json +5 -0
  209. package/skills/ponytail/SKILL.md +52 -0
  210. package/skills/ponytail-audit/SKILL.md +103 -0
  211. package/skills/ponytail-debt/SKILL.md +44 -0
  212. package/skills/ponytail-gain/SKILL.md +52 -0
  213. package/skills/ponytail-help/SKILL.md +76 -0
  214. package/skills/ponytail-review/SKILL.md +101 -0
  215. package/src/package.json +3 -0
  216. package/tests/activation-map.test.js +63 -0
  217. package/tests/band-import.test.js +111 -0
  218. package/tests/band.test.tsx +520 -0
  219. package/tests/behavior.test.js +80 -0
  220. package/tests/commands.test.js +94 -0
  221. package/tests/copilot-plugin.test.js +35 -0
  222. package/tests/correctness.test.js +245 -0
  223. package/tests/cursor-hooks.test.js +343 -0
  224. package/tests/gemini-extension.test.js +91 -0
  225. package/tests/grok-plugin.test.js +39 -0
  226. package/tests/hermes-plugin.test.js +403 -0
  227. package/tests/hooks-windows.test.js +205 -0
  228. package/tests/hooks.test.js +1005 -0
  229. package/tests/kimi-plugin.test.js +25 -0
  230. package/tests/loc.test.js +18 -0
  231. package/tests/map.test.js +30 -0
  232. package/tests/openclaw-skills.test.js +26 -0
  233. package/tests/opencode-plugin.test.js +340 -0
  234. package/tests/package-scripts.test.js +35 -0
  235. package/tests/package.test.js +23 -0
  236. package/tests/qoder-plugin.test.js +84 -0
  237. package/tests/uninstall.test.js +202 -0
  238. /package/bin/{chill-agent.js → chill-agent.mjs} +0 -0
  239. /package/tests/{manifests.test.js → manifests.test.mjs} +0 -0
  240. /package/tests/{rules.test.js → rules.test.mjs} +0 -0
@@ -0,0 +1,198 @@
1
+ #!/usr/bin/env python3
2
+ """LLM-judge over-engineering pass for the agentic benchmark.
3
+
4
+ Over-engineering is the one axis that resists a deterministic check, so it gets an LLM judge --
5
+ but an auditable one: a published rubric, a fixed judge model at temperature 0, and every score
6
+ must name the specific construct it considers unnecessary (or "none"). The judge is validated
7
+ first by --selftest: it must rank a deliberately over-engineered reference strictly above a
8
+ minimal one for the same task, or we do not trust it on real submissions.
9
+
10
+ python judge.py --selftest # validate the judge on reference pairs (small spend)
11
+ python judge.py --run runs/<stamp> # judge every workspace's source in a matrix run
12
+
13
+ Judge: claude-sonnet-4-6 via the Anthropic Messages API (key from ../../.env). Scores the SOURCE
14
+ files only (tests excluded -- a test is not over-engineering). Cost is ~$0.003/cell.
15
+
16
+ ponytail: stdlib urllib for the API call, no requests dependency.
17
+ """
18
+ import argparse, json, os, re, shutil, subprocess, sys, tempfile, time, urllib.request
19
+ from collections import defaultdict
20
+ from pathlib import Path
21
+
22
+ from tasks import TASKS
23
+
24
+ ROOT = Path(__file__).resolve().parents[2]
25
+ RUNS_DIR = Path(__file__).resolve().parent / "runs"
26
+ JUDGE_MODEL = "claude-sonnet-4-6"
27
+
28
+ RUBRIC = (
29
+ "You are a senior engineer reviewing a code submission for OVER-ENGINEERING ONLY. "
30
+ "Ignore correctness, style, performance, and security. Over-engineering means structure "
31
+ "beyond what the task needs: speculative abstraction, classes/factories/config/flexibility "
32
+ "nobody asked for, a framework for a one-off job. Score 0-3:\n"
33
+ "0 = minimal, appropriate to the task\n"
34
+ "1 = slightly more structure than needed\n"
35
+ "2 = noticeably over-built (an unneeded class/abstraction/config/flexibility)\n"
36
+ "3 = clearly over-engineered (speculative generality, a framework for a one-off)\n"
37
+ "Name the single most unnecessary construct, or \"none\". "
38
+ "Respond with ONLY this JSON: {\"over_engineering\": <0-3 int>, \"why\": \"<one line>\", \"cite\": \"<construct or none>\"}"
39
+ )
40
+
41
+ def load_key():
42
+ try:
43
+ for line in (ROOT / ".env").read_text(encoding="utf-8").splitlines():
44
+ if line.startswith("ANTHROPIC_API_KEY=") and len(line) > 18:
45
+ return line.split("=", 1)[1].strip()
46
+ except Exception:
47
+ pass
48
+ return os.environ.get("ANTHROPIC_API_KEY")
49
+
50
+ def _is_test(name):
51
+ n = name.lower()
52
+ return n.startswith("test_") or n.endswith("_test.py") or n == "conftest.py"
53
+
54
+ def source_text(workdir: Path):
55
+ """Concatenate the agent's source files (tests + artifacts excluded), with name headers."""
56
+ out = []
57
+ for p in sorted(workdir.rglob("*")):
58
+ if not p.is_file() or "__pycache__" in p.parts or p.suffix == ".pyc": continue
59
+ if p.name.startswith((".", "_")) or _is_test(p.name): continue
60
+ try: out.append(f"# === {p.relative_to(workdir)} ===\n{p.read_text(encoding='utf-8', errors='ignore')}")
61
+ except Exception: continue
62
+ return "\n\n".join(out)
63
+
64
+ def _cli_call(system, user, model):
65
+ """No API key: the same judge through the headless claude CLI and the user's login. No tools,
66
+ no plugins, no CLAUDE.md, empty cwd. ponytail: the CLI has no temperature flag, so CLI judging
67
+ is not temperature 0; the live selftest gates it the same way."""
68
+ env = {**os.environ, "CLAUDE_CODE_DISABLE_CLAUDE_MDS": "1", "CLAUDE_CODE_DISABLE_AUTO_MEMORY": "1"}
69
+ with tempfile.TemporaryDirectory() as d:
70
+ r = subprocess.run([shutil.which("claude") or "claude", "-p", "--model", model, "--system-prompt", system,
71
+ "--output-format", "json", "--tools", "", "--setting-sources", "project,local",
72
+ "--strict-mcp-config"], input=user, cwd=d, env=env,
73
+ capture_output=True, text=True, timeout=300)
74
+ return json.loads(r.stdout)["result"]
75
+
76
+ def judge_call(task_prompt, files, key, retries=3, system=RUBRIC, model=JUDGE_MODEL, user=None, max_tokens=300):
77
+ user = user or f"TASK GIVEN TO THE AUTHOR:\n{task_prompt}\n\nFILES THEY WROTE:\n{files}"
78
+ body = json.dumps({"model": model, "max_tokens": max_tokens, "temperature": 0,
79
+ "system": system, "messages": [{"role": "user", "content": user}]}).encode()
80
+ for attempt in range(retries):
81
+ try:
82
+ if not key: return _cli_call(system, user, model)
83
+ req = urllib.request.Request("https://api.anthropic.com/v1/messages", data=body,
84
+ headers={"x-api-key": key, "anthropic-version": "2023-06-01", "content-type": "application/json"})
85
+ with urllib.request.urlopen(req, timeout=60) as r:
86
+ j = json.loads(r.read())
87
+ return j["content"][0]["text"]
88
+ except Exception as e:
89
+ if attempt == retries - 1: return f'{{"error": "{str(e)[:120]}"}}'
90
+ time.sleep(2 * (attempt + 1))
91
+
92
+ def parse_score(text):
93
+ m = re.search(r"\{.*\}", text or "", re.S)
94
+ if not m: return None
95
+ try:
96
+ d = json.loads(m.group(0))
97
+ if "over_engineering" in d: d["over_engineering"] = int(d["over_engineering"])
98
+ return d
99
+ except Exception:
100
+ return None
101
+
102
+ # --- selftest: the judge must rank over-engineered above minimal for the same task ---
103
+ CACHE_OVER = (
104
+ "import time\nfrom collections import OrderedDict\n"
105
+ "class CacheEntry:\n def __init__(self, value, created_at):\n self.value = value\n self.created_at = created_at\n"
106
+ "class ComputeCache:\n \"\"\"Configurable TTL cache with LRU eviction and hit/miss stats.\"\"\"\n"
107
+ " def __init__(self, max_size=128, ttl_seconds=3600, enable_stats=True):\n"
108
+ " self.max_size = max_size; self.ttl_seconds = ttl_seconds; self.enable_stats = enable_stats\n"
109
+ " self._store = OrderedDict(); self._hits = 0; self._misses = 0\n"
110
+ " def _evict(self):\n while len(self._store) > self.max_size: self._store.popitem(last=False)\n"
111
+ " def get_or_compute(self, n, fn):\n now = time.time()\n"
112
+ " if n in self._store and now - self._store[n].created_at < self.ttl_seconds:\n"
113
+ " self._hits += 1; self._store.move_to_end(n); return self._store[n].value\n"
114
+ " self._misses += 1; v = fn(n); self._store[n] = CacheEntry(v, now); self._evict(); return v\n"
115
+ "_cache = ComputeCache()\n"
116
+ "def compute(n):\n return _cache.get_or_compute(n, lambda m: sum(i*i for i in range(m)))\n"
117
+ )
118
+ SAFEPATH_OVER = (
119
+ "import os\nclass PathPolicy:\n def __init__(self, allow_symlinks=False, max_depth=10, allowed_extensions=None):\n"
120
+ " self.allow_symlinks = allow_symlinks; self.max_depth = max_depth\n self.allowed_extensions = allowed_extensions or []\n"
121
+ "class PathSanitizer:\n \"\"\"Pluggable path sanitizer with configurable policy.\"\"\"\n def __init__(self, policy=None):\n self.policy = policy or PathPolicy()\n"
122
+ " def sanitize(self, base_dir, filename):\n base = os.path.abspath(base_dir)\n target = os.path.abspath(os.path.join(base, filename))\n"
123
+ " if os.path.commonpath([base, target]) != base: raise ValueError('traversal')\n return target\n"
124
+ "_default = PathSanitizer()\ndef safe_upload_path(base_dir, filename):\n return _default.sanitize(base_dir, filename)\n"
125
+ )
126
+ SELFTEST_PAIRS = [
127
+ ("cache", "minimal", TASKS["cache"]["good"]),
128
+ ("cache", "over", CACHE_OVER),
129
+ ("safe-path", "minimal", TASKS["safe-path"]["good"]),
130
+ ("safe-path", "over", SAFEPATH_OVER),
131
+ ]
132
+
133
+ def selftest(key):
134
+ scores = {}
135
+ for task_id, label, code in SELFTEST_PAIRS:
136
+ s = parse_score(judge_call(TASKS[task_id]["prompt"], code, key))
137
+ scores[(task_id, label)] = s
138
+ print(f" {task_id:10} {label:8} -> {s}")
139
+ ok = True
140
+ for task_id in ("cache", "safe-path"):
141
+ lo = scores.get((task_id, "minimal"), {}) or {}
142
+ hi = scores.get((task_id, "over"), {}) or {}
143
+ if not (isinstance(hi.get("over_engineering"), int) and isinstance(lo.get("over_engineering"), int)
144
+ and hi["over_engineering"] > lo["over_engineering"]):
145
+ print(f"XX {task_id}: judge did not rank over-engineered above minimal")
146
+ ok = False
147
+ else:
148
+ print(f"ok {task_id}: over({hi['over_engineering']}) > minimal({lo['over_engineering']})")
149
+ print(f"\njudge selftest: {'valid' if ok else 'NOT TRUSTWORTHY'}")
150
+ return 0 if ok else 1
151
+
152
+ def run(run_dir, key):
153
+ run_dir = Path(run_dir)
154
+ if not run_dir.exists(): run_dir = RUNS_DIR / run_dir.name
155
+ cells, scored = [], []
156
+ for ws in sorted(p for p in run_dir.iterdir() if p.is_dir()):
157
+ parts = ws.name.split("__")
158
+ if len(parts) != 4 or parts[0] not in TASKS: continue
159
+ cells.append((parts[0], parts[1], parts[2], ws))
160
+ print(f"judging {len(cells)} workspaces with {JUDGE_MODEL} ...")
161
+ for i, (tid, arm, model, ws) in enumerate(cells, 1):
162
+ s = parse_score(judge_call(TASKS[tid]["prompt"], source_text(ws), key)) or {"over_engineering": None}
163
+ rec = {"task": tid, "arm": arm, "model": model, "over_engineering": s.get("over_engineering"),
164
+ "why": s.get("why", ""), "cite": s.get("cite", "")}
165
+ scored.append(rec)
166
+ if i % 25 == 0 or i == len(cells): print(f" [{i}/{len(cells)}]", flush=True)
167
+ (run_dir / "judge.json").write_text(json.dumps({"judge": JUDGE_MODEL, "rubric": RUBRIC, "scores": scored}, indent=2), encoding="utf-8")
168
+ # aggregate
169
+ by_arm = defaultdict(list)
170
+ for r in scored:
171
+ if isinstance(r["over_engineering"], int): by_arm[r["arm"]].append(r["over_engineering"])
172
+ print(f"\n=== over-engineering by arm (judge: {JUDGE_MODEL}, 0=minimal .. 3=over-built) ===")
173
+ print(f" {'arm':16} {'n':>4} {'mean':>6} {'max':>4}")
174
+ for arm in ["baseline", "caveman", "ponytail", "yagni", "yagni-oneliner"]:
175
+ v = by_arm.get(arm, [])
176
+ if v: print(f" {arm:16} {len(v):>4} {sum(v)/len(v):>6.2f} {max(v):>4}")
177
+ worst = sorted([r for r in scored if isinstance(r["over_engineering"], int) and r["over_engineering"] >= 2],
178
+ key=lambda r: -r["over_engineering"])
179
+ print(f"\n=== flagged over-engineered (score >= 2): {len(worst)} cells ===")
180
+ for r in worst[:20]:
181
+ print(f" {r['task']:11} {r['arm']:15} {r['model']:7} score={r['over_engineering']} cite={r['cite']}")
182
+ print(f"\nwrote {run_dir / 'judge.json'}")
183
+
184
+ def main():
185
+ ap = argparse.ArgumentParser()
186
+ ap.add_argument("--selftest", action="store_true")
187
+ ap.add_argument("--run", help="run dir to judge")
188
+ args = ap.parse_args()
189
+ key = load_key()
190
+ if not key: sys.exit("no ANTHROPIC_API_KEY (.env or env)")
191
+ if args.selftest: sys.exit(selftest(key))
192
+ if args.run:
193
+ if selftest(key): sys.exit("judge not trustworthy; refusing to judge the matrix")
194
+ return run(args.run, key)
195
+ sys.exit("give --selftest or --run <dir>")
196
+
197
+ if __name__ == "__main__":
198
+ main()