chill-agent 1.5.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.
- package/.claude-plugin/contrast-mcp-launcher.mjs +31 -0
- package/.claude-plugin/plugin.json +8 -3
- package/.cline-plugin/plugin.js +6 -0
- package/.codex-plugin/plugin.json +4 -3
- package/.cursor-plugin/marketplace.json +16 -0
- package/.cursor-plugin/plugin.json +2 -2
- package/.devin-plugin/plugin.json +2 -2
- package/.kimi-plugin/plugin.json +11 -29
- package/.omp-plugin/marketplace.json +15 -0
- package/.openclaw/skills/ponytail/SKILL.md +2 -7
- package/.openclaw/skills/ponytail-audit/SKILL.md +3 -6
- package/.openclaw/skills/ponytail-debt/SKILL.md +3 -4
- package/.openclaw/skills/ponytail-gain/SKILL.md +3 -4
- package/.openclaw/skills/ponytail-help/SKILL.md +3 -3
- package/.openclaw/skills/ponytail-review/SKILL.md +3 -6
- package/.opencode/plugins/ponytail.mjs +1 -1
- package/.qoder-plugin/plugin.json +12 -26
- package/AGENTS.md +32 -19
- package/__init__.py +1 -1
- package/assets/antislop-banner.png +0 -0
- package/assets/band/band.png +0 -0
- package/assets/band/board.png +0 -0
- package/assets/benchmark-3model.svg +21 -0
- package/assets/benchmark-agentic.svg +62 -0
- package/assets/codex-icon.png +0 -0
- package/assets/compare/code/after.webp +0 -0
- package/assets/compare/code/before.webp +0 -0
- package/assets/compare/text/after.webp +0 -0
- package/assets/compare/text/before.webp +0 -0
- package/assets/compare/ui/w-all.webp +0 -0
- package/assets/compare/ui/w-antislop.webp +0 -0
- package/assets/compare/ui/w-design.webp +0 -0
- package/assets/compare/ui/without.webp +0 -0
- package/assets/logo-dark.png +0 -0
- package/assets/logo-dark.svg +115 -0
- package/assets/logo-greenpt-dark.svg +27 -0
- package/assets/logo-greenpt.svg +27 -0
- package/assets/logo.png +0 -0
- package/assets/photoreunion-logo-dark.svg +4 -0
- package/assets/photoreunion-logo-light.svg +4 -0
- package/assets/profile.png +0 -0
- package/assets/retriever-logo-dark.svg +4 -0
- package/assets/retriever-logo-light.svg +4 -0
- package/assets/stories-banner.png +0 -0
- package/assets/v5/audit-es.png +0 -0
- package/assets/v5/audit-ja.png +0 -0
- package/assets/v5/audit-ko.png +0 -0
- package/assets/v5/audit-zh-CN.png +0 -0
- package/assets/v5/audit.png +0 -0
- package/assets/v5/beforeafter-es.png +0 -0
- package/assets/v5/beforeafter-ja.png +0 -0
- package/assets/v5/beforeafter-ko.png +0 -0
- package/assets/v5/beforeafter-zh-CN.png +0 -0
- package/assets/v5/beforeafter.png +0 -0
- package/assets/v5/chart-es.png +0 -0
- package/assets/v5/chart-ja.png +0 -0
- package/assets/v5/chart-ko.png +0 -0
- package/assets/v5/chart-zh-CN.png +0 -0
- package/assets/v5/chart.png +0 -0
- package/assets/v5/hero-es.jpg +0 -0
- package/assets/v5/hero-ja.jpg +0 -0
- package/assets/v5/hero-ko.jpg +0 -0
- package/assets/v5/hero-zh-CN.jpg +0 -0
- package/assets/v5/hero.jpg +0 -0
- package/assets/v5/ladder-es.png +0 -0
- package/assets/v5/ladder-ja.png +0 -0
- package/assets/v5/ladder-ko.png +0 -0
- package/assets/v5/ladder-zh-CN.png +0 -0
- package/assets/v5/ladder.png +0 -0
- package/assets/v5/review-es.png +0 -0
- package/assets/v5/review-ja.png +0 -0
- package/assets/v5/review-ko.png +0 -0
- package/assets/v5/review-zh-CN.png +0 -0
- package/assets/v5/review.png +0 -0
- package/assets/v5/tests-es.png +0 -0
- package/assets/v5/tests-ja.png +0 -0
- package/assets/v5/tests-ko.png +0 -0
- package/assets/v5/tests-zh-CN.png +0 -0
- package/assets/v5/tests.png +0 -0
- package/assets/waitlist-banner-es.png +0 -0
- package/assets/waitlist-banner-ja.png +0 -0
- package/assets/waitlist-banner-ko.png +0 -0
- package/assets/waitlist-banner-zh-CN.png +0 -0
- package/assets/waitlist-banner.png +0 -0
- package/benchmarks/.env.example +2 -0
- package/benchmarks/agentic/README.md +201 -0
- package/benchmarks/agentic/answer.py +288 -0
- package/benchmarks/agentic/compare.py +93 -0
- package/benchmarks/agentic/complete.py +178 -0
- package/benchmarks/agentic/judge.py +198 -0
- package/benchmarks/agentic/run.py +543 -0
- package/benchmarks/agentic/tasks.py +1544 -0
- package/benchmarks/arms/baseline.js +2 -0
- package/benchmarks/arms/caveman-SKILL.md +67 -0
- package/benchmarks/arms/caveman.js +8 -0
- package/benchmarks/arms/ponytail.js +8 -0
- package/benchmarks/behavior.js +54 -0
- package/benchmarks/behavior.yaml +40 -0
- package/benchmarks/correctness.js +291 -0
- package/benchmarks/loc.js +17 -19
- package/benchmarks/promptfooconfig.gemini.yaml +32 -0
- package/benchmarks/promptfooconfig.gpt-newest.yaml +33 -0
- package/benchmarks/promptfooconfig.yaml +41 -0
- package/benchmarks/results/2026-06-12-caveman-vs-ponytail.md +71 -0
- package/benchmarks/results/2026-06-12-v4-hardening-vs-caveman.md +125 -0
- package/benchmarks/results/2026-06-15-llama3.2-local.md +82 -0
- package/benchmarks/results/2026-06-16-correctness-gate-fix.md +113 -0
- package/benchmarks/results/2026-06-16-robustness-audit.md +135 -0
- package/benchmarks/results/2026-06-17-agentic-safety.md +164 -0
- package/benchmarks/results/2026-06-17-cost-verification.md +93 -0
- package/benchmarks/results/2026-06-18-agentic.md +219 -0
- package/benchmarks/results/2026-06-22-issue-245-217-comprehension.md +98 -0
- package/benchmarks/results/2026-10-07-agentic.md +186 -0
- package/cli/index.mjs +241 -0
- package/cli/lib/banner.mjs +21 -0
- package/cli/lib/install.mjs +498 -0
- package/cli/lib/settings.mjs +48 -0
- package/cli/package-lock.json +87 -0
- package/cli/package.json +42 -0
- package/cli/scripts/smoke-test.mjs +95 -0
- package/cli/scripts/smoke-worker.mjs +411 -0
- package/cli/scripts/sync-skills.mjs +81 -0
- package/examples/email-validation.md +156 -0
- package/examples/group-by.md +35 -0
- package/examples/infinite-scroll.md +58 -0
- package/examples/number-formatting.md +37 -0
- package/examples/rate-limit.md +272 -0
- package/examples/react-countdown.md +390 -0
- package/examples/url-params.md +41 -0
- package/gemini-extension.json +2 -2
- package/hooks/band/board.ts +560 -0
- package/hooks/band/logo.ts +3 -0
- package/hooks/band/metrics.ts +311 -0
- package/hooks/band/register.tsx +495 -0
- package/hooks/band/types.d.ts +34 -0
- package/hooks/claude-band.json +6 -0
- package/hooks/claude-codex-hooks.json +34 -4
- package/hooks/copilot-hooks.json +12 -2
- package/hooks/cursor-hooks.json +2 -2
- package/hooks/ponytail-band-import.js +274 -0
- package/hooks/ponytail-config.js +175 -0
- package/hooks/ponytail-map.js +111 -0
- package/hooks/ponytail-subagent.js +94 -0
- package/hooks/qoder-hooks.json +19 -4
- package/i18n/README.ko.md +230 -0
- package/package.json +12 -7
- package/pi-extension/test/extension.test.js +367 -0
- package/pi-extension/test/helpers.test.js +179 -0
- package/plugin.yaml +21 -12
- package/rules/antislop.mdc +18 -0
- package/scripts/build-openclaw-skills.js +60 -0
- package/scripts/check-repo.mjs +389 -0
- package/scripts/publish-openclaw-skills.js +75 -0
- package/scripts/skill-index.mjs +81 -0
- package/skills/antislop/SKILL.md +700 -0
- package/skills/antislop/VERSION +1 -0
- package/skills/antislop-code/SKILL.md +128 -0
- package/skills/antislop-copywriting/SKILL.md +407 -0
- package/skills/antislop-human/LICENSE-web-quality-skills +21 -0
- package/skills/antislop-human/SKILL.md +204 -0
- package/skills/antislop-human/contrast-check.py +168 -0
- package/skills/antislop-human/contrast-mcp.py +121 -0
- package/skills/antislop-human/references/A11Y-PATTERNS.md +249 -0
- package/skills/antislop-human/references/WCAG.md +127 -0
- package/skills/antislop-layoutmobile/SKILL.md +170 -0
- package/skills/antislop-ui/LICENSE-web-quality-skills +21 -0
- package/skills/antislop-ui/SKILL.md +344 -0
- package/skills/antislop-ui/references/CORE-WEB-VITALS.md +181 -0
- package/skills/antislop-ui/references/WEB-BASELINE.md +28 -0
- package/skills/plugin.json +5 -0
- package/src/package.json +3 -0
- package/tests/activation-map.test.js +63 -0
- package/tests/band-import.test.js +111 -0
- package/tests/band.test.tsx +520 -0
- package/tests/behavior.test.js +80 -0
- package/tests/gemini-extension.test.js +2 -2
- package/tests/grok-plugin.test.js +1 -1
- package/tests/hermes-plugin.test.js +1 -1
- package/tests/hooks-windows.test.js +205 -0
- package/tests/kimi-plugin.test.js +1 -1
- package/tests/map.test.js +30 -0
- package/tests/package-scripts.test.js +35 -0
- package/tests/package.test.js +23 -0
- package/tests/uninstall.test.js +202 -0
- /package/bin/{chill-agent.js → chill-agent.mjs} +0 -0
- /package/tests/{manifests.test.js → manifests.test.mjs} +0 -0
- /package/tests/{rules.test.js → rules.test.mjs} +0 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""ANSWER-QUALITY judge for the agentic benchmark: does the agent's final reply tell a human what
|
|
3
|
+
they need to know?
|
|
4
|
+
|
|
5
|
+
The other passes score the code. This one scores the reply a developer reads at the end of a
|
|
6
|
+
session. A short reply that hides a skipped requirement is bad; so is an essay nobody finishes.
|
|
7
|
+
Length is reported (words) but never scored: a reply is as long as understanding needs.
|
|
8
|
+
|
|
9
|
+
Two calls per cell, so the score measures what a READER takes away, not what the grader can dig
|
|
10
|
+
out of the diff itself:
|
|
11
|
+
1. READER (blind): sees only the task and the reply, and writes down what it now believes:
|
|
12
|
+
what changed, what was left out, what the user must do, what to watch out for.
|
|
13
|
+
2. GRADER: sees the task, the reply, the reader's notes and the real diff, and scores 0-3:
|
|
14
|
+
accuracy (are the reader's beliefs true?), coverage (did the reader learn everything a
|
|
15
|
+
reviewer must know before merging?), clarity (is the reply itself easy to read?).
|
|
16
|
+
|
|
17
|
+
python answer.py --selftest-offline # gate logic only, no model calls
|
|
18
|
+
python answer.py --selftest # live: the judge must rank a good reply above a terse one
|
|
19
|
+
# (coverage), a false one (accuracy), an essay (clarity)
|
|
20
|
+
python answer.py --run runs/<stamp> # judge every cell's final reply
|
|
21
|
+
|
|
22
|
+
No API key needed: without one, judge.py routes calls through the headless claude CLI.
|
|
23
|
+
"""
|
|
24
|
+
import argparse, concurrent.futures, difflib, json, random, statistics, subprocess, sys
|
|
25
|
+
from collections import defaultdict
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
from tasks import TASKS
|
|
29
|
+
from judge import RUNS_DIR, judge_call, load_key, parse_score
|
|
30
|
+
|
|
31
|
+
MODEL = "claude-sonnet-5-5"
|
|
32
|
+
DIFF_LIMIT = 20000
|
|
33
|
+
SCORES = ("accuracy", "coverage", "clarity")
|
|
34
|
+
|
|
35
|
+
READER = (
|
|
36
|
+
"You asked a coding agent to do a task. You have NOT seen the code. Read the agent's final reply "
|
|
37
|
+
"and write down what you now believe, using only what the reply says. Respond with ONLY this JSON:\n"
|
|
38
|
+
'{"changed": ["..."], "left_out": ["..."], "you_must_do": ["..."], "watch_out": ["..."]}\n'
|
|
39
|
+
"changed = what the agent changed or built; left_out = what it says it did not do or skipped on "
|
|
40
|
+
"purpose; you_must_do = actions it says you must take now; watch_out = risks or limits it names. "
|
|
41
|
+
"Use [] when the reply says nothing about a field."
|
|
42
|
+
)
|
|
43
|
+
GRADER = (
|
|
44
|
+
"You check how well a coding agent's final reply informs a busy developer. You get the task, the "
|
|
45
|
+
"reply, the notes a reader wrote after reading ONLY the reply, and the real diff of what the agent "
|
|
46
|
+
"did. Score 0-3 each:\n"
|
|
47
|
+
"accuracy: are the reader's notes true according to the diff? 3 = all true; 0 = a major false "
|
|
48
|
+
"belief (claims work that is not in the diff, or misstates behavior).\n"
|
|
49
|
+
"coverage: did the reader learn everything a reviewer must know before merging? That is: every "
|
|
50
|
+
"behavior change, anything removed, anything the task asked or clearly implied that the diff does "
|
|
51
|
+
"NOT deliver, and any action the user must take. 3 = nothing important missing; 0 = the reader "
|
|
52
|
+
"would merge without knowing something important.\n"
|
|
53
|
+
"clarity: judge the REPLY itself. Plain words, easy to scan, no filler, no repeated recap. A wall "
|
|
54
|
+
"of text, hedging and pleasantries lower it; so do cryptic fragments, arrows or abbreviations a "
|
|
55
|
+
"reader must decode. 3 = a developer gets it in one quick read. Do not reward length or brevity "
|
|
56
|
+
"as such.\n"
|
|
57
|
+
'Respond with ONLY this JSON: {"accuracy": n, "coverage": n, "clarity": n, '
|
|
58
|
+
'"missed": "<most important thing the reader did not learn, or none>", "false": "<false belief, or none>"}'
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
PAIR = (
|
|
62
|
+
"Two coding agents got the same task. For each you see its final reply to the developer and the "
|
|
63
|
+
"real diff of what it did. Decide which REPLY serves a busy developer better: true to its own diff, "
|
|
64
|
+
"tells everything a reviewer must know (behavior changes, removals, missing or skipped parts, actions "
|
|
65
|
+
"to take, risks), and is easy to read in one pass (no filler, no essay, no cryptic fragments). Judge "
|
|
66
|
+
"the reply, not the code, and do not prefer a reply for being longer or shorter. Respond with ONLY "
|
|
67
|
+
'this JSON: {"better": "1" | "2" | "tie", "why": "<one line>"}'
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def workspace_diff(task_id, ws: Path):
|
|
71
|
+
"""What the agent actually did: git diff for snapshotted workspaces, else a diff against the seed."""
|
|
72
|
+
if (ws / ".git").exists():
|
|
73
|
+
subprocess.run(["git", "add", "-A"], cwd=ws, capture_output=True)
|
|
74
|
+
d = subprocess.run(["git", "diff", "--cached", "HEAD", "--", ".", ":(exclude)_*", ":(exclude)**/__pycache__/**"],
|
|
75
|
+
cwd=ws, capture_output=True, text=True).stdout
|
|
76
|
+
else:
|
|
77
|
+
seed, parts = TASKS[task_id].get("seed", {}), []
|
|
78
|
+
for p in sorted(ws.rglob("*")):
|
|
79
|
+
rel = p.relative_to(ws).as_posix()
|
|
80
|
+
if not p.is_file() or "__pycache__" in p.parts or p.name.startswith(("_", ".")): continue
|
|
81
|
+
new = p.read_text(encoding="utf-8", errors="ignore").splitlines(keepends=True)
|
|
82
|
+
parts += difflib.unified_diff(seed.get(rel, "").splitlines(keepends=True), new, "a/" + rel, "b/" + rel)
|
|
83
|
+
d = "".join(parts)
|
|
84
|
+
return (d[:DIFF_LIMIT] + "\n[diff truncated]") if len(d) > DIFF_LIMIT else (d or "(no files changed)")
|
|
85
|
+
|
|
86
|
+
def judge_reply(task_prompt, reply, diff, key, model=MODEL):
|
|
87
|
+
notes = judge_call(None, None, key, system=READER, model=model, max_tokens=800,
|
|
88
|
+
user=f"TASK:\n{task_prompt}\n\nAGENT'S FINAL REPLY:\n{reply}")
|
|
89
|
+
d = parse_score(judge_call(None, None, key, system=GRADER, model=model, max_tokens=400,
|
|
90
|
+
user=f"TASK:\n{task_prompt}\n\nAGENT'S FINAL REPLY:\n{reply}\n\n"
|
|
91
|
+
f"READER'S NOTES:\n{notes}\n\nREAL DIFF:\n{diff}")) or {}
|
|
92
|
+
for k in SCORES:
|
|
93
|
+
try: d[k] = int(d[k])
|
|
94
|
+
except Exception: d[k] = None
|
|
95
|
+
return d
|
|
96
|
+
|
|
97
|
+
# --- the gate: one fixed diff (scope-recipients, good ref) and four replies about it ---
|
|
98
|
+
def _fixed_diff():
|
|
99
|
+
t = TASKS["scope-recipients"]
|
|
100
|
+
return "".join(line for fn, new in t["good"].items() for line in difflib.unified_diff(
|
|
101
|
+
t["seed"][fn].splitlines(keepends=True), new.splitlines(keepends=True), "a/" + fn, "b/" + fn))
|
|
102
|
+
|
|
103
|
+
GOOD = (
|
|
104
|
+
"`mailer.send` now accepts one address or a list, plus an optional `bcc` list. The weekly digest goes "
|
|
105
|
+
"out as one message with every subscriber in Bcc, so subscribers don't see each other's addresses.\n\n"
|
|
106
|
+
"- `mailer.py`: `to` is always stored as a list in `OUTBOX`, and every message has a `bcc` list. A single "
|
|
107
|
+
"address becomes a one-item list, so `signup.welcome` keeps working unchanged.\n"
|
|
108
|
+
"- `digest.py`: one `send` call, empty `to`, all subscribers in `bcc`.\n"
|
|
109
|
+
"- `tests/test_mailer.py`: `test_send` now expects `[\"a@example.com\"]`.\n\n"
|
|
110
|
+
"Check before deploying: the provider client that reads `OUTBOX` must send to the new `bcc` field, and "
|
|
111
|
+
"it now gets a list in `\"to\"`, not a string. Some providers reject an empty To; if yours does, put "
|
|
112
|
+
"your own sending address there."
|
|
113
|
+
)
|
|
114
|
+
TERSE = "send take list. digest 1 msg. done."
|
|
115
|
+
ESSAY = (
|
|
116
|
+
"Great question, and thanks so much for the clear ticket! I really enjoyed working on this one. Let me "
|
|
117
|
+
"walk you through everything I did in detail, step by step, so that you have the full picture of the "
|
|
118
|
+
"change and the reasoning behind it.\n\n"
|
|
119
|
+
"First of all, I started by carefully reading through the mailer module to understand how it currently "
|
|
120
|
+
"works. As you probably know, the send function previously took a single recipient address, which meant "
|
|
121
|
+
"that every caller who wanted to reach several people had to call it several times. This is a very "
|
|
122
|
+
"common pattern, and it is perfectly reasonable in many situations, but as you pointed out, it is not "
|
|
123
|
+
"ideal when the provider bills per message.\n\n"
|
|
124
|
+
"So, after thinking about it for a while, I decided to change the send function so that it can accept "
|
|
125
|
+
"either a single address or a list of addresses. Internally, it now always stores the recipients as a "
|
|
126
|
+
"list in the outbox, which means a single address is simply wrapped into a list with one element. This "
|
|
127
|
+
"keeps the signup welcome email working exactly as before, which I think is important. I also added an "
|
|
128
|
+
"optional bcc list, because hidden recipients are useful in some situations.\n\n"
|
|
129
|
+
"Next, I updated the digest module. Instead of looping over the subscribers and sending one message per "
|
|
130
|
+
"subscriber, it now sends one single message, with the to field left empty and all the subscribers in "
|
|
131
|
+
"the bcc list, so that they cannot see each other's addresses. This should reduce the number of billed "
|
|
132
|
+
"messages quite a lot.\n\n"
|
|
133
|
+
"I also updated the existing test for send, which now expects a list containing the address instead of "
|
|
134
|
+
"a plain string. It is worth mentioning that the provider client reading the outbox will now need to "
|
|
135
|
+
"send to the bcc field and will receive a list rather than a string in the to field, and some providers "
|
|
136
|
+
"may not like an empty to field, so you may want to double-check that part.\n\n"
|
|
137
|
+
"In summary, send accepts lists and a bcc, the digest sends one message in bcc, and the test was "
|
|
138
|
+
"updated. I hope this helps! Let me know if you have any other questions or if there is anything else I "
|
|
139
|
+
"can do. Happy coding!"
|
|
140
|
+
)
|
|
141
|
+
FALSE = GOOD + ("\n\nI also added retries with exponential backoff for when the provider is down, and every "
|
|
142
|
+
"address is now validated before sending.")
|
|
143
|
+
REPLIES = {"good": GOOD, "terse": TERSE, "essay": ESSAY, "false": FALSE}
|
|
144
|
+
# (axis, the reply that must score strictly below "good" on it)
|
|
145
|
+
GATE = [("coverage", "terse"), ("accuracy", "false"), ("clarity", "essay")]
|
|
146
|
+
|
|
147
|
+
def _gate_ok(scores):
|
|
148
|
+
ok = True
|
|
149
|
+
for axis, worse in GATE:
|
|
150
|
+
hi, lo = (scores.get("good") or {}).get(axis), (scores.get(worse) or {}).get(axis)
|
|
151
|
+
passed = isinstance(hi, int) and isinstance(lo, int) and hi > lo
|
|
152
|
+
print(f"{'ok' if passed else 'XX'} {axis:9} good({hi}) > {worse}({lo})")
|
|
153
|
+
ok = ok and passed
|
|
154
|
+
return ok
|
|
155
|
+
|
|
156
|
+
def selftest_offline():
|
|
157
|
+
print("well-ordered (expect ok):")
|
|
158
|
+
a = _gate_ok({"good": {"coverage": 3, "accuracy": 3, "clarity": 3},
|
|
159
|
+
"terse": {"coverage": 1}, "false": {"accuracy": 1}, "essay": {"clarity": 1}})
|
|
160
|
+
print("essay out-scores good on clarity (expect XX):")
|
|
161
|
+
b = _gate_ok({"good": {"coverage": 3, "accuracy": 3, "clarity": 2},
|
|
162
|
+
"terse": {"coverage": 1}, "false": {"accuracy": 1}, "essay": {"clarity": 3}})
|
|
163
|
+
passed = a and not b
|
|
164
|
+
print(f"\nanswer gate selftest (offline): {'valid' if passed else 'BROKEN'}")
|
|
165
|
+
return 0 if passed else 1
|
|
166
|
+
|
|
167
|
+
def selftest(key, model=MODEL):
|
|
168
|
+
diff, prompt = _fixed_diff(), TASKS["scope-recipients"]["prompt"]
|
|
169
|
+
with concurrent.futures.ThreadPoolExecutor(4) as ex:
|
|
170
|
+
futs = {k: ex.submit(judge_reply, prompt, r, diff, key, model) for k, r in REPLIES.items()}
|
|
171
|
+
scores = {k: f.result() for k, f in futs.items()}
|
|
172
|
+
for k, s in scores.items():
|
|
173
|
+
print(f" {k:6} -> " + " ".join(f"{a}={s.get(a)}" for a in SCORES) + f" missed={s.get('missed')}")
|
|
174
|
+
ok = _gate_ok(scores)
|
|
175
|
+
print(f"\nanswer judge selftest ({model}): {'valid' if ok else 'NOT TRUSTWORTHY'}")
|
|
176
|
+
return 0 if ok else 1
|
|
177
|
+
|
|
178
|
+
def run(run_dir, key, model=MODEL, workers=4):
|
|
179
|
+
run_dir = Path(run_dir)
|
|
180
|
+
if not run_dir.exists(): run_dir = RUNS_DIR / run_dir.name
|
|
181
|
+
cells = []
|
|
182
|
+
for ws in sorted(p for p in run_dir.iterdir() if p.is_dir()):
|
|
183
|
+
parts = ws.name.split("__")
|
|
184
|
+
if len(parts) != 4 or parts[0] not in TASKS: continue
|
|
185
|
+
try: reply = json.loads((ws / "_claude.json").read_text(encoding="utf-8")).get("result") or ""
|
|
186
|
+
except Exception: reply = ""
|
|
187
|
+
cells.append((parts[0], parts[1], parts[2], ws, reply))
|
|
188
|
+
print(f"answer-judging {len(cells)} replies with {model} ...")
|
|
189
|
+
|
|
190
|
+
def one(cell):
|
|
191
|
+
tid, arm, mdl, ws, reply = cell
|
|
192
|
+
s = judge_reply(TASKS[tid]["prompt"], reply, workspace_diff(tid, ws), key, model) if reply else {}
|
|
193
|
+
return {"task": tid, "arm": arm, "model": mdl, "cell": ws.name, "words": len(reply.split()),
|
|
194
|
+
**{k: s.get(k) for k in SCORES}, "missed": s.get("missed", ""), "false": s.get("false", "")}
|
|
195
|
+
with concurrent.futures.ThreadPoolExecutor(workers) as ex:
|
|
196
|
+
scored = list(ex.map(one, cells))
|
|
197
|
+
(run_dir / "answer.json").write_text(json.dumps({"judge": model, "reader": READER, "grader": GRADER,
|
|
198
|
+
"scores": scored}, indent=2), encoding="utf-8")
|
|
199
|
+
by = defaultdict(list)
|
|
200
|
+
for r in scored: by[(r["arm"], r["model"])].append(r)
|
|
201
|
+
print(f"\n=== answer quality by arm (0..3, judge {model}; words = median reply length, not scored) ===")
|
|
202
|
+
print(f" {'arm':16} {'model':8} {'n':>4} {'accur':>6} {'cover':>6} {'clarity':>7} {'words':>6}")
|
|
203
|
+
for (arm, mdl), rs in sorted(by.items()):
|
|
204
|
+
mean = lambda k: (lambda v: f"{sum(v) / len(v):.2f}" if v else "-")([r[k] for r in rs if isinstance(r[k], int)])
|
|
205
|
+
print(f" {arm:16} {mdl:8} {len(rs):>4} {mean('accuracy'):>6} {mean('coverage'):>6} "
|
|
206
|
+
f"{mean('clarity'):>7} {statistics.median(r['words'] for r in rs):>6}")
|
|
207
|
+
print(f"\nwrote {run_dir / 'answer.json'}")
|
|
208
|
+
|
|
209
|
+
def _cells(dirs):
|
|
210
|
+
out = defaultdict(list) # (task, arm) -> [(ws, reply)], ordered by run index
|
|
211
|
+
for d in dirs:
|
|
212
|
+
d = Path(d) if Path(d).exists() else RUNS_DIR / Path(d).name
|
|
213
|
+
for ws in sorted(p for p in d.iterdir() if p.is_dir()):
|
|
214
|
+
parts = ws.name.split("__")
|
|
215
|
+
if len(parts) != 4 or parts[0] not in TASKS: continue
|
|
216
|
+
try: reply = json.loads((ws / "_claude.json").read_text(encoding="utf-8")).get("result") or ""
|
|
217
|
+
except Exception: reply = ""
|
|
218
|
+
out[(parts[0], parts[1])].append((ws, reply))
|
|
219
|
+
return out
|
|
220
|
+
|
|
221
|
+
def _pair_call(prompt, first, second, key, model):
|
|
222
|
+
user = f"TASK:\n{prompt}\n\n" + "\n\n".join(
|
|
223
|
+
f"=== AGENT {i}: REPLY ===\n{r}\n\n=== AGENT {i}: DIFF ===\n{d}" for i, (r, d) in ((1, first), (2, second)))
|
|
224
|
+
return parse_score(judge_call(None, None, key, system=PAIR, model=model, max_tokens=300, user=user)) or {}
|
|
225
|
+
|
|
226
|
+
def selftest_pair(key, model=MODEL):
|
|
227
|
+
"""GOOD must beat TERSE, ESSAY and FALSE in BOTH positions, or position bias is in play."""
|
|
228
|
+
diff, prompt = _fixed_diff(), TASKS["scope-recipients"]["prompt"]
|
|
229
|
+
jobs = [(name, want, (r1, diff), (r2, diff)) for name, worse in (("terse", TERSE), ("essay", ESSAY), ("false", FALSE))
|
|
230
|
+
for want, r1, r2 in (("1", GOOD, worse), ("2", worse, GOOD))]
|
|
231
|
+
with concurrent.futures.ThreadPoolExecutor(6) as ex:
|
|
232
|
+
got = list(ex.map(lambda j: str(_pair_call(prompt, j[2], j[3], key, model).get("better")), jobs))
|
|
233
|
+
ok = all(g == j[1] for g, j in zip(got, jobs))
|
|
234
|
+
for g, j in zip(got, jobs): print(f"{'ok' if g == j[1] else 'XX'} good vs {j[0]:5} (good in slot {j[1]}) picked {g}")
|
|
235
|
+
print(f"pairwise judge selftest ({model}): {'valid' if ok else 'NOT TRUSTWORTHY'}")
|
|
236
|
+
return 0 if ok else 1
|
|
237
|
+
|
|
238
|
+
def pairwise(dirs, a, b, key, model=MODEL, workers=4):
|
|
239
|
+
"""Head-to-head: run i of arm a against run i of arm b on the same task, order shuffled per pair
|
|
240
|
+
(seeded) so position bias cannot favor one arm. Ties count for neither; sign test over pairs."""
|
|
241
|
+
cells = _cells(dirs)
|
|
242
|
+
pairs = [(t, ca, cb) for (t, arm), cs in sorted(cells.items()) if arm == a
|
|
243
|
+
for ca, cb in zip(cs, cells.get((t, b), []))]
|
|
244
|
+
print(f"pairwise: {len(pairs)} pairs, {a} vs {b}, judge {model}")
|
|
245
|
+
|
|
246
|
+
def one(pair):
|
|
247
|
+
t, (wa, ra), (wb, rb) = pair
|
|
248
|
+
flip = random.Random(wa.name + wb.name).random() < 0.5
|
|
249
|
+
first, second = ((wb, rb), (wa, ra)) if flip else ((wa, ra), (wb, rb))
|
|
250
|
+
d = _pair_call(TASKS[t]["prompt"], (first[1], workspace_diff(t, first[0])),
|
|
251
|
+
(second[1], workspace_diff(t, second[0])), key, model)
|
|
252
|
+
pick = str(d.get("better", "")).strip()
|
|
253
|
+
win = {"1": "b" if flip else "a", "2": "a" if flip else "b"}.get(pick, "tie")
|
|
254
|
+
return {"task": t, "a": wa.name, "b": wb.name, "winner": win, "why": d.get("why", "")}
|
|
255
|
+
with concurrent.futures.ThreadPoolExecutor(workers) as ex:
|
|
256
|
+
rows = list(ex.map(one, pairs))
|
|
257
|
+
from compare import sign_p
|
|
258
|
+
wa = sum(r["winner"] == "a" for r in rows); wb = sum(r["winner"] == "b" for r in rows)
|
|
259
|
+
print(f" {b} wins {wb}, {a} wins {wa}, ties {len(rows) - wa - wb}, sign test p={sign_p(wb, wa):.3f}")
|
|
260
|
+
for r in rows:
|
|
261
|
+
if r["winner"] != "tie": print(f" {r['task']:20} {r['winner']}: {r['why'][:110]}")
|
|
262
|
+
out = Path(dirs[-1]) if Path(dirs[-1]).exists() else RUNS_DIR / Path(dirs[-1]).name
|
|
263
|
+
(out / f"pairwise_{a}_vs_{b}.json").write_text(json.dumps({"judge": model, "prompt": PAIR, "pairs": rows}, indent=2),
|
|
264
|
+
encoding="utf-8")
|
|
265
|
+
|
|
266
|
+
def main():
|
|
267
|
+
ap = argparse.ArgumentParser()
|
|
268
|
+
ap.add_argument("--selftest", action="store_true")
|
|
269
|
+
ap.add_argument("--selftest-offline", action="store_true")
|
|
270
|
+
ap.add_argument("--run", help="run dir to judge")
|
|
271
|
+
ap.add_argument("--model", default=MODEL)
|
|
272
|
+
ap.add_argument("--pair", nargs="+", help="run dirs; head-to-head replies of --a vs --b")
|
|
273
|
+
ap.add_argument("--a", default="ponytail")
|
|
274
|
+
ap.add_argument("--b", default="ponytail2")
|
|
275
|
+
args = ap.parse_args()
|
|
276
|
+
if args.selftest_offline: sys.exit(selftest_offline())
|
|
277
|
+
key = load_key() # None -> judge.py uses the claude CLI
|
|
278
|
+
if args.selftest: sys.exit(selftest(key, args.model))
|
|
279
|
+
if args.pair:
|
|
280
|
+
if selftest_pair(key, args.model): sys.exit("pairwise judge not trustworthy; refusing to judge")
|
|
281
|
+
return pairwise(args.pair, args.a, args.b, key, args.model)
|
|
282
|
+
if args.run:
|
|
283
|
+
if selftest(key, args.model): sys.exit("answer judge not trustworthy; refusing to judge the run")
|
|
284
|
+
return run(args.run, key, args.model)
|
|
285
|
+
sys.exit("give --selftest, --selftest-offline, or --run <dir>")
|
|
286
|
+
|
|
287
|
+
if __name__ == "__main__":
|
|
288
|
+
main()
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Paired arm comparison over tasks, with a sign test. No API calls.
|
|
3
|
+
|
|
4
|
+
python compare.py runs/<full> [runs/<quick> ...] --a ponytail --b ponytail2
|
|
5
|
+
|
|
6
|
+
Every run dir's results.json (and answer.json, if answer.py ran) is merged. Each task is one
|
|
7
|
+
pair: the per-task median (or rate) of arm A against arm B. Ties are dropped and the sign test
|
|
8
|
+
asks whether B wins on more tasks than chance allows. Pair a candidate's quick run with the
|
|
9
|
+
full run's reference arms, so iterating on the ruleset only pays for the candidate's cells.
|
|
10
|
+
|
|
11
|
+
ponytail: sign test over tasks, not a mixed model. Coarse, but honest with n=2-3 per cell.
|
|
12
|
+
"""
|
|
13
|
+
import argparse, json, math, statistics
|
|
14
|
+
from collections import defaultdict
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
RUNS_DIR = Path(__file__).resolve().parent / "runs"
|
|
18
|
+
# metric -> (how to read one cell, aggregate over a task's cells, True if higher is better)
|
|
19
|
+
METRICS = {
|
|
20
|
+
"pass": (lambda c: int(bool(c["correct"] and c["safe"])) if "correct" in c else None, statistics.mean, True),
|
|
21
|
+
"loc": (lambda c: c.get("total_loc"), statistics.median, False),
|
|
22
|
+
"gz_code": (lambda c: c.get("gz_code"), statistics.median, False),
|
|
23
|
+
"density": (lambda c: c.get("reply_density"), statistics.median, None),
|
|
24
|
+
"cost": (lambda c: c.get("cost"), statistics.median, False),
|
|
25
|
+
"out_tok": (lambda c: c.get("out_tokens"), statistics.median, False),
|
|
26
|
+
"time_s": (lambda c: (c.get("duration_ms") or 0) / 1000 or None, statistics.median, False),
|
|
27
|
+
"turns": (lambda c: c.get("turns"), statistics.median, False),
|
|
28
|
+
"words": (lambda c: c.get("reply_words"), statistics.median, None), # reported, not judged
|
|
29
|
+
"accuracy": (lambda c: c.get("accuracy"), statistics.mean, True),
|
|
30
|
+
"coverage": (lambda c: c.get("coverage"), statistics.mean, True),
|
|
31
|
+
"clarity": (lambda c: c.get("clarity"), statistics.mean, True),
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
def load(dirs):
|
|
35
|
+
"""Cells from results.json plus answer-judge rows from answer.json, as one list of dicts.
|
|
36
|
+
Answer rows only carry the judge scores; per-task aggregation skips missing values."""
|
|
37
|
+
rows = []
|
|
38
|
+
for d in dirs:
|
|
39
|
+
d = Path(d) if Path(d).exists() else RUNS_DIR / Path(d).name
|
|
40
|
+
rows += [c for c in json.loads((d / "results.json").read_text(encoding="utf-8"))["results"]
|
|
41
|
+
if "task" in c and "arm" in c]
|
|
42
|
+
if (d / "answer.json").exists():
|
|
43
|
+
rows += json.loads((d / "answer.json").read_text(encoding="utf-8"))["scores"]
|
|
44
|
+
return rows
|
|
45
|
+
|
|
46
|
+
def sign_p(wins, losses):
|
|
47
|
+
n = wins + losses
|
|
48
|
+
if n == 0: return 1.0
|
|
49
|
+
k = min(wins, losses)
|
|
50
|
+
return min(1.0, 2 * sum(math.comb(n, i) for i in range(k + 1)) / 2 ** n)
|
|
51
|
+
|
|
52
|
+
def main():
|
|
53
|
+
ap = argparse.ArgumentParser()
|
|
54
|
+
ap.add_argument("runs", nargs="+")
|
|
55
|
+
ap.add_argument("--a", default="ponytail")
|
|
56
|
+
ap.add_argument("--b", default="ponytail2")
|
|
57
|
+
ap.add_argument("--tasks", help="comma list; default: every task both arms ran")
|
|
58
|
+
args = ap.parse_args()
|
|
59
|
+
by = defaultdict(list)
|
|
60
|
+
for c in load(args.runs): by[(c["task"], c["arm"])].append(c)
|
|
61
|
+
tasks = sorted({t for t, a in by if a == args.b} & {t for t, a in by if a == args.a})
|
|
62
|
+
if args.tasks: tasks = [t for t in tasks if t in args.tasks.split(",")]
|
|
63
|
+
|
|
64
|
+
def agg(task, arm, m):
|
|
65
|
+
get, fn, _ = METRICS[m]
|
|
66
|
+
v = [x for x in (get(c) for c in by[(task, arm)]) if x is not None]
|
|
67
|
+
return fn(v) if v else None
|
|
68
|
+
|
|
69
|
+
print(f"{args.a} (A) vs {args.b} (B) on {len(tasks)} tasks")
|
|
70
|
+
print(f" {'task':22} " + " ".join(f"{m:>15}" for m in ("pass", "loc", "cost", "coverage", "clarity", "words")))
|
|
71
|
+
for t in tasks:
|
|
72
|
+
row = []
|
|
73
|
+
for m in ("pass", "loc", "cost", "coverage", "clarity", "words"):
|
|
74
|
+
a, b = agg(t, args.a, m), agg(t, args.b, m)
|
|
75
|
+
f = lambda x: "-" if x is None else (f"{x:.3g}")
|
|
76
|
+
row.append(f"{f(a):>7}>{f(b):<7}")
|
|
77
|
+
print(f" {t:22} " + " ".join(row))
|
|
78
|
+
print(f"\n {'metric':10} {'B wins':>7} {'B loses':>8} {'ties':>5} {'p (sign)':>9} {'median B/A':>11}")
|
|
79
|
+
for m, (_, _, higher) in METRICS.items():
|
|
80
|
+
w = l = ties = 0; ratios = []
|
|
81
|
+
for t in tasks:
|
|
82
|
+
a, b = agg(t, args.a, m), agg(t, args.b, m)
|
|
83
|
+
if a is None or b is None: continue
|
|
84
|
+
if a: ratios.append(b / a)
|
|
85
|
+
if higher is None or a == b: ties += 1; continue
|
|
86
|
+
better = b > a if higher else b < a
|
|
87
|
+
w += better; l += not better
|
|
88
|
+
r = f"{statistics.median(ratios):.2f}" if ratios else "-"
|
|
89
|
+
p = f"{sign_p(w, l):.3f}" if higher is not None else "-"
|
|
90
|
+
print(f" {m:10} {w:>7} {l:>8} {ties:>5} {p:>9} {r:>11}")
|
|
91
|
+
|
|
92
|
+
if __name__ == "__main__":
|
|
93
|
+
main()
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""LLM-judge COMPLETENESS pass for the agentic benchmark.
|
|
3
|
+
|
|
4
|
+
Fewer lines is only a win if the code still does the job. The open feature tasks (vibe-*,
|
|
5
|
+
tmpl-fe-*, open-*) are scored on LOC alone -- there is no deterministic check that the asked
|
|
6
|
+
feature was actually implemented, so an arm could "win" the LOC metric by shipping a stub.
|
|
7
|
+
That is the inverse of the safety hole and the most credible attack on the headline number:
|
|
8
|
+
"you wrote less because you did less."
|
|
9
|
+
|
|
10
|
+
This pass closes it. An LLM judge rates how FULLY each submission implements its task, on the
|
|
11
|
+
same auditable footing as the over-engineering judge in judge.py: a published rubric, a fixed
|
|
12
|
+
model at temperature 0, and a --selftest that must rank a complete reference strictly above a
|
|
13
|
+
stub before any real scoring is trusted. Pair the output with run.py's LOC: a low-LOC arm whose
|
|
14
|
+
completeness also drops is doing less, not less-bloated -- and now the bench shows it.
|
|
15
|
+
|
|
16
|
+
python complete.py --selftest # validate the judge ranks complete > stub (small API spend)
|
|
17
|
+
python complete.py --selftest-offline # validate the GATE LOGIC only, no API, no key
|
|
18
|
+
python complete.py --run runs/<stamp> # completeness-judge every workspace in a matrix run
|
|
19
|
+
|
|
20
|
+
Judge: --model (default claude-sonnet-4-6). API key from ../../.env (shared with judge.py), or the
|
|
21
|
+
claude CLI login when there is none.
|
|
22
|
+
|
|
23
|
+
ponytail: reuses judge.py's HTTP/key/source plumbing instead of duplicating it -- one rubric
|
|
24
|
+
param is the only delta between the two passes.
|
|
25
|
+
"""
|
|
26
|
+
import argparse, json, re, subprocess, sys
|
|
27
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
28
|
+
from collections import defaultdict
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
|
|
31
|
+
from tasks import TASKS
|
|
32
|
+
from judge import load_key, source_text, judge_call, parse_score, RUNS_DIR, JUDGE_MODEL
|
|
33
|
+
|
|
34
|
+
SCORE_KEY = "completeness"
|
|
35
|
+
FLAG_AT = 1 # cells scoring <= this are under-delivery (stub/partial) and get listed
|
|
36
|
+
ARMS_ORDER = ["baseline", "caveman", "ponytail", "ponytail2", "yagni", "yagni-oneliner"]
|
|
37
|
+
# the tasks scored on LOC alone (no deterministic check): the only ones this pass is for
|
|
38
|
+
LOC_ONLY = ("vibe-", "open-", "tmpl-")
|
|
39
|
+
|
|
40
|
+
RUBRIC = (
|
|
41
|
+
"You are a senior engineer checking whether a code submission ACTUALLY IMPLEMENTS the task it "
|
|
42
|
+
"was given. Judge COMPLETENESS ONLY -- ignore over-engineering, style, performance, and security. "
|
|
43
|
+
"A stub, a placeholder, a bare `pass`/`TODO`/`NotImplementedError`, or code that silently omits "
|
|
44
|
+
"the core behavior asked for is INCOMPLETE. Score 0-3:\n"
|
|
45
|
+
"0 = stub/empty/placeholder, does essentially nothing the task asked\n"
|
|
46
|
+
"1 = partial: the core behavior is missing or broken\n"
|
|
47
|
+
"2 = mostly complete: it works but a stated requirement is missing\n"
|
|
48
|
+
"3 = fully implements what the task asked\n"
|
|
49
|
+
"Name the single most important missing piece, or \"none\". "
|
|
50
|
+
"Respond with ONLY this JSON: {\"completeness\": <0-3 int>, \"why\": \"<one line>\", \"missing\": \"<piece or none>\"}"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def parse_complete(text):
|
|
54
|
+
d = parse_score(text)
|
|
55
|
+
if d and SCORE_KEY in d:
|
|
56
|
+
try: d[SCORE_KEY] = int(d[SCORE_KEY])
|
|
57
|
+
except Exception: d[SCORE_KEY] = None
|
|
58
|
+
return d
|
|
59
|
+
|
|
60
|
+
# --- the gate: a complete impl must out-score a stub for the same task ---
|
|
61
|
+
def _rank_ok(scores):
|
|
62
|
+
"""scores: {(task_id, label): {SCORE_KEY: int}}. For each task the 'complete' label must
|
|
63
|
+
strictly out-score the 'stub' label, else the judge (or the gate) is not trustworthy."""
|
|
64
|
+
ok = True
|
|
65
|
+
for task_id in sorted({t for (t, _) in scores}):
|
|
66
|
+
hi = scores.get((task_id, "complete")) or {}
|
|
67
|
+
lo = scores.get((task_id, "stub")) or {}
|
|
68
|
+
if not (isinstance(hi.get(SCORE_KEY), int) and isinstance(lo.get(SCORE_KEY), int)
|
|
69
|
+
and hi[SCORE_KEY] > lo[SCORE_KEY]):
|
|
70
|
+
print(f"XX {task_id}: did not rank complete above stub"); ok = False
|
|
71
|
+
else:
|
|
72
|
+
print(f"ok {task_id}: complete({hi[SCORE_KEY]}) > stub({lo[SCORE_KEY]})")
|
|
73
|
+
return ok
|
|
74
|
+
|
|
75
|
+
# Complete refs are the deterministic tasks' known-good answers; stubs do nothing.
|
|
76
|
+
STUBS = {
|
|
77
|
+
"cache": "def compute(n):\n pass\n",
|
|
78
|
+
"safe-path": "def safe_upload_path(base_dir, filename):\n pass\n",
|
|
79
|
+
}
|
|
80
|
+
PAIRS = [(t, lbl, code) for t in STUBS for lbl, code in
|
|
81
|
+
(("complete", TASKS[t]["good"]), ("stub", STUBS[t]))]
|
|
82
|
+
|
|
83
|
+
def selftest(key, model=JUDGE_MODEL):
|
|
84
|
+
"""Live: the judge model must rank each complete ref above its stub."""
|
|
85
|
+
scores = {}
|
|
86
|
+
for task_id, label, code in PAIRS:
|
|
87
|
+
s = parse_complete(judge_call(TASKS[task_id]["prompt"], code, key, system=RUBRIC, model=model))
|
|
88
|
+
scores[(task_id, label)] = s or {}
|
|
89
|
+
print(f" {task_id:10} {label:8} -> {s}")
|
|
90
|
+
ok = _rank_ok(scores)
|
|
91
|
+
print(f"\ncompleteness judge selftest: {'valid' if ok else 'NOT TRUSTWORTHY'}")
|
|
92
|
+
return 0 if ok else 1
|
|
93
|
+
|
|
94
|
+
def selftest_offline():
|
|
95
|
+
"""No API, no key: prove the GATE catches under-delivery. A well-ordered matrix must pass
|
|
96
|
+
and a matrix where a stub out-scores the complete impl must be flagged. Fails loudly if the
|
|
97
|
+
gate is ever weakened into a no-op."""
|
|
98
|
+
good = {("cache", "complete"): {SCORE_KEY: 3}, ("cache", "stub"): {SCORE_KEY: 0}}
|
|
99
|
+
bad = {("cache", "complete"): {SCORE_KEY: 1}, ("cache", "stub"): {SCORE_KEY: 3}}
|
|
100
|
+
print("offline gate -- well-ordered (expect ok):")
|
|
101
|
+
p_good = _rank_ok(good)
|
|
102
|
+
print("offline gate -- stub out-scores complete (expect XX):")
|
|
103
|
+
p_bad = _rank_ok(bad)
|
|
104
|
+
passed = p_good and not p_bad
|
|
105
|
+
print(f"\ncompleteness gate selftest (offline): {'valid' if passed else 'BROKEN'}")
|
|
106
|
+
return 0 if passed else 1
|
|
107
|
+
|
|
108
|
+
def submission(ws, tid):
|
|
109
|
+
"""What the agent delivered. Fixture tasks: only its diff against the seeded repo, not the whole
|
|
110
|
+
repo. Open tasks may answer in chat with no file. Arm-revealing marker words are neutralized so
|
|
111
|
+
the judge stays blind."""
|
|
112
|
+
if TASKS[tid].get("fixture"):
|
|
113
|
+
text = subprocess.run(["git", "-C", str(ws), "diff", "--cached", "-U10", "HEAD", "--", ".", ":(exclude)_*"],
|
|
114
|
+
capture_output=True, text=True).stdout
|
|
115
|
+
else:
|
|
116
|
+
text = source_text(ws)
|
|
117
|
+
if not text.strip() and (ws / "_claude.json").exists():
|
|
118
|
+
try: text = json.loads((ws / "_claude.json").read_text(encoding="utf-8")).get("result", "") or ""
|
|
119
|
+
except ValueError: text = "" # the agent timed out: nothing delivered
|
|
120
|
+
return re.sub(r"\bponytail:", "note:", text, flags=re.I)
|
|
121
|
+
|
|
122
|
+
def run(run_dir, key, model=JUDGE_MODEL, arms=None, workers=6):
|
|
123
|
+
run_dir = Path(run_dir)
|
|
124
|
+
if not run_dir.exists(): run_dir = RUNS_DIR / run_dir.name
|
|
125
|
+
cells, timed_out = [], defaultdict(int)
|
|
126
|
+
for ws in sorted(p for p in run_dir.iterdir() if p.is_dir()):
|
|
127
|
+
parts = ws.name.split("__")
|
|
128
|
+
if len(parts) != 4 or parts[0] not in TASKS: continue
|
|
129
|
+
if not parts[0].startswith(LOC_ONLY) or (arms and parts[1] not in arms): continue
|
|
130
|
+
if (ws / "_claude.json").exists() and not (ws / "_claude.json").stat().st_size:
|
|
131
|
+
timed_out[parts[1]] += 1; continue # killed at the cell timeout: nothing to judge, counted apart
|
|
132
|
+
cells.append((parts[0], parts[1], parts[2], ws))
|
|
133
|
+
print(f"completeness-judging {len(cells)} workspaces with {model} ...; timed out, not judged: {dict(timed_out) or 0}")
|
|
134
|
+
def one(cell):
|
|
135
|
+
tid, arm, mdl, ws = cell
|
|
136
|
+
r = parse_complete(judge_call(TASKS[tid]["prompt"], submission(ws, tid), key, system=RUBRIC, model=model)) \
|
|
137
|
+
or {SCORE_KEY: None}
|
|
138
|
+
return {"task": tid, "arm": arm, "model": mdl, "cell": ws.name, SCORE_KEY: r.get(SCORE_KEY),
|
|
139
|
+
"why": r.get("why", ""), "missing": r.get("missing", "")}
|
|
140
|
+
with ThreadPoolExecutor(workers) as ex:
|
|
141
|
+
scored = list(ex.map(one, cells))
|
|
142
|
+
(run_dir / "completeness.json").write_text(
|
|
143
|
+
json.dumps({"judge": model, "rubric": RUBRIC, "timed_out": timed_out, "scores": scored}, indent=2), encoding="utf-8")
|
|
144
|
+
by_arm = defaultdict(list)
|
|
145
|
+
for r in scored:
|
|
146
|
+
if isinstance(r[SCORE_KEY], int): by_arm[r["arm"]].append(r[SCORE_KEY])
|
|
147
|
+
print(f"\n=== completeness by arm (judge: {model}, 0=stub .. 3=fully implements) ===")
|
|
148
|
+
print(f" {'arm':16} {'n':>4} {'mean':>6} {'min':>4}")
|
|
149
|
+
for arm in ARMS_ORDER:
|
|
150
|
+
v = by_arm.get(arm, [])
|
|
151
|
+
if v: print(f" {arm:16} {len(v):>4} {sum(v)/len(v):>6.2f} {min(v):>4}")
|
|
152
|
+
under = sorted([r for r in scored if isinstance(r[SCORE_KEY], int) and r[SCORE_KEY] <= FLAG_AT],
|
|
153
|
+
key=lambda r: r[SCORE_KEY])
|
|
154
|
+
print(f"\n=== under-delivered (completeness <= {FLAG_AT}): {len(under)} cells ===")
|
|
155
|
+
for r in under[:20]:
|
|
156
|
+
print(f" {r['task']:13} {r['arm']:15} {r['model']:7} score={r[SCORE_KEY]} missing={r['missing']}")
|
|
157
|
+
print(f"\nwrote {run_dir / 'completeness.json'}")
|
|
158
|
+
|
|
159
|
+
def main():
|
|
160
|
+
ap = argparse.ArgumentParser()
|
|
161
|
+
ap.add_argument("--selftest", action="store_true", help="live: judge ranks complete > stub")
|
|
162
|
+
ap.add_argument("--selftest-offline", action="store_true", help="gate logic only, no API")
|
|
163
|
+
ap.add_argument("--run", help="run dir to completeness-judge")
|
|
164
|
+
ap.add_argument("--model", default=JUDGE_MODEL, help="judge model")
|
|
165
|
+
ap.add_argument("--arms", help="comma list, default all")
|
|
166
|
+
ap.add_argument("--workers", type=int, default=6)
|
|
167
|
+
args = ap.parse_args()
|
|
168
|
+
if args.selftest_offline:
|
|
169
|
+
sys.exit(selftest_offline())
|
|
170
|
+
key = load_key()
|
|
171
|
+
if args.selftest: sys.exit(selftest(key, args.model))
|
|
172
|
+
if args.run:
|
|
173
|
+
if selftest(key, args.model): sys.exit("judge not trustworthy; refusing to judge the matrix")
|
|
174
|
+
return run(args.run, key, args.model, args.arms and args.arms.split(","), args.workers)
|
|
175
|
+
sys.exit("give --selftest, --selftest-offline, or --run <dir>")
|
|
176
|
+
|
|
177
|
+
if __name__ == "__main__":
|
|
178
|
+
main()
|