@matt82198/aesop 0.1.0 → 0.3.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.
- package/CHANGELOG.md +117 -2
- package/README.md +59 -218
- package/bin/cli.js +168 -41
- package/daemons/run-watchdog.sh +16 -4
- package/daemons/selfheal.sh +231 -0
- package/docs/ANY-REPO.md +427 -0
- package/docs/CONTRIBUTING.md +72 -0
- package/docs/DEMO.md +334 -0
- package/docs/HOOK-INSTALL.md +15 -56
- package/docs/INSTALL.md +74 -4
- package/docs/PORTING.md +166 -0
- package/docs/README.md +33 -3
- package/docs/TEAM-STATE.md +372 -0
- package/docs/reproduce.md +33 -3
- package/driver/CLAUDE.md +150 -0
- package/driver/README.md +383 -0
- package/driver/aesop.config.example.json +80 -0
- package/driver/agent_driver.py +355 -0
- package/driver/backend_config.py +253 -0
- package/driver/claude_code_driver.py +198 -0
- package/driver/codex_driver.py +673 -0
- package/driver/openai_compatible_driver.py +249 -0
- package/driver/openai_transport.py +179 -0
- package/driver/verification_policy.py +75 -0
- package/driver/wave_bridge.py +254 -0
- package/driver/wave_loop.py +1408 -0
- package/driver/wave_scheduler.py +890 -0
- package/hooks/pre-push-policy.sh +131 -33
- package/mcp/server.mjs +320 -4
- package/monitor/collect-signals.mjs +69 -0
- package/package.json +22 -14
- package/skills/CLAUDE.md +132 -2
- package/skills/buildsystem/SKILL.md +330 -0
- package/skills/buildsystem/wave-flat-dispatch.template.mjs +658 -0
- package/skills/fleet/SKILL.md +113 -0
- package/skills/power/SKILL.md +246 -131
- package/state_store/__init__.py +4 -2
- package/state_store/api.py +19 -4
- package/state_store/coordination.py +209 -0
- package/state_store/identity.py +51 -0
- package/state_store/projections.py +63 -0
- package/state_store/read_api.py +156 -0
- package/state_store/store.py +185 -73
- package/state_store/write_api.py +462 -0
- package/templates/wave-presets/data.json +65 -0
- package/templates/wave-presets/library.json +65 -0
- package/templates/wave-presets/saas.json +64 -0
- package/tools/audit_report.py +388 -0
- package/tools/bench_runner.py +100 -3
- package/tools/ci_merge_wait.py +256 -35
- package/tools/ci_workflow_lint.py +430 -0
- package/tools/claudemd_drift.py +394 -0
- package/tools/claudemd_lint.py +359 -0
- package/tools/common.py +39 -3
- package/tools/cost_ceiling.py +166 -43
- package/tools/cost_econ.py +480 -0
- package/tools/cost_projection.py +559 -0
- package/tools/crossos_drift.py +394 -0
- package/tools/defect_escape.py +252 -0
- package/tools/doctor.js +1 -1
- package/tools/eod_sweep.py +188 -26
- package/tools/fleet.js +260 -0
- package/tools/fleet_ledger.py +209 -7
- package/tools/git_identity_check.py +315 -0
- package/tools/health-score.js +40 -0
- package/tools/health_score.py +361 -0
- package/tools/metrics_gate.py +13 -4
- package/tools/mutation_test.py +523 -0
- package/tools/portability_check.py +206 -0
- package/tools/proposals.mjs +47 -2
- package/tools/reconcile.py +7 -4
- package/tools/reproduce.js +405 -0
- package/tools/secret_scan.py +207 -65
- package/tools/self_stats.py +20 -0
- package/tools/stall_check.py +247 -16
- package/tools/stateapi_lint.py +325 -0
- package/tools/test_battery.py +173 -0
- package/tools/transcript_digest.py +380 -0
- package/tools/verify_activity_filter.py +437 -0
- package/tools/verify_agent_inspector.py +2 -0
- package/tools/verify_cost_panel.py +345 -0
- package/tools/verify_dash.py +2 -0
- package/tools/verify_dispatch_panel.py +301 -0
- package/tools/verify_failure_drilldown.py +188 -0
- package/tools/verify_prboard.py +2 -0
- package/tools/verify_scorecards.py +281 -0
- package/tools/verify_submit_encoding.py +2 -0
- package/tools/verify_ui_trio.py +409 -0
- package/tools/verify_wave_telemetry.py +268 -0
- package/tools/wave_backlog_analyzer.py +490 -0
- package/tools/wave_ledger_hook.py +150 -0
- package/tools/wave_preflight.py +779 -0
- package/tools/wave_resume.py +215 -0
- package/tools/wave_templates.py +340 -0
- package/ui/agents.py +68 -14
- package/ui/collectors.py +81 -55
- package/ui/config.py +7 -2
- package/ui/cost.py +231 -12
- package/ui/handler.py +383 -55
- package/ui/quality_scorecard.py +232 -0
- package/ui/sse.py +3 -3
- package/ui/wave_audit_tail.py +213 -0
- package/ui/wave_dispatch.py +280 -0
- package/ui/wave_failure.py +288 -0
- package/ui/wave_gantt.py +152 -0
- package/ui/wave_reasoning_tail.py +176 -0
- package/ui/wave_telemetry.py +383 -0
- package/ui/web/dist/assets/index-CNQxaiOW.css +1 -0
- package/ui/web/dist/assets/index-CP68RIh3.js +9 -0
- package/ui/web/dist/index.html +2 -2
- package/bin/CLAUDE.md +0 -76
- package/daemons/CLAUDE.md +0 -36
- package/dash/CLAUDE.md +0 -32
- package/docs/archive/README.md +0 -3
- package/docs/archive/spikes/tiered-cognition/ACTIVATION.md +0 -125
- package/docs/archive/spikes/tiered-cognition/DESIGN.md +0 -287
- package/docs/archive/spikes/tiered-cognition/FINDINGS.md +0 -113
- package/docs/archive/spikes/tiered-cognition/README.md +0 -27
- package/docs/archive/spikes/tiered-cognition/aesop-cognition.example.md +0 -32
- package/docs/archive/spikes/tiered-cognition/force-model-policy.merged.mjs +0 -673
- package/docs/archive/spikes/tiered-cognition/strip-tools-hook.mjs +0 -434
- package/hooks/CLAUDE.md +0 -89
- package/mcp/CLAUDE.md +0 -213
- package/monitor/CLAUDE.md +0 -40
- package/scan/CLAUDE.md +0 -30
- package/state_store/CLAUDE.md +0 -39
- package/tools/CLAUDE.md +0 -79
- package/ui/CLAUDE.md +0 -127
- package/ui/web/dist/assets/index-0qQYnvMC.js +0 -9
- package/ui/web/dist/assets/index-BdIlFieV.css +0 -1
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Deterministic audit report generator — aggregates machine-generated audit outputs.
|
|
4
|
+
|
|
5
|
+
Assembles findings from multiple tools into a single dated markdown audit report:
|
|
6
|
+
- defect_escape.py telemetry (code quality metrics)
|
|
7
|
+
- mutation_test.py results (test quality gaps)
|
|
8
|
+
- claudemd_lint.py findings (documentation integrity)
|
|
9
|
+
- OUTCOMES-LEDGER.md verdict rates (fleet reliability)
|
|
10
|
+
|
|
11
|
+
All sources are optional by default (graceful degradation). Use --strict to require all.
|
|
12
|
+
Output is deterministic (no LLM calls, pure aggregation).
|
|
13
|
+
|
|
14
|
+
CLI:
|
|
15
|
+
python tools/audit_report.py [--defect-escape=<json>] [--mutation-test=<json>]
|
|
16
|
+
[--claudemd-lint=<json>] [--ledger=<path>]
|
|
17
|
+
[--out=<path>] [--strict]
|
|
18
|
+
|
|
19
|
+
Exit: 0=success, 1=error (missing sources in --strict mode, read errors, etc.)
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import argparse
|
|
23
|
+
import json
|
|
24
|
+
import sys
|
|
25
|
+
from datetime import datetime, timezone
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Any, Dict, Optional, Tuple
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def load_json_file(path: Optional[str]) -> Tuple[Optional[Dict[str, Any]], Optional[str]]:
|
|
31
|
+
"""Load a JSON file.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
path: Path to JSON file or None
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Tuple of (data, error_msg). If path is None, returns (None, None).
|
|
38
|
+
On error, returns (None, error_msg).
|
|
39
|
+
"""
|
|
40
|
+
if path is None:
|
|
41
|
+
return None, None
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
# Use utf-8-sig to handle BOM on Windows
|
|
45
|
+
with open(path, "r", encoding="utf-8-sig") as f:
|
|
46
|
+
data = json.load(f)
|
|
47
|
+
return data, None
|
|
48
|
+
except FileNotFoundError:
|
|
49
|
+
return None, f"File not found: {path}"
|
|
50
|
+
except json.JSONDecodeError as e:
|
|
51
|
+
return None, f"Invalid JSON in {path}: {e}"
|
|
52
|
+
except IOError as e:
|
|
53
|
+
return None, f"Failed to read {path}: {e}"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def load_ledger_file(path: Optional[str]) -> Tuple[Optional[str], Optional[str]]:
|
|
57
|
+
"""Load a ledger markdown file.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
path: Path to ledger file or None
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Tuple of (content, error_msg). If path is None, returns (None, None).
|
|
64
|
+
"""
|
|
65
|
+
if path is None:
|
|
66
|
+
return None, None
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
70
|
+
content = f.read()
|
|
71
|
+
return content, None
|
|
72
|
+
except FileNotFoundError:
|
|
73
|
+
return None, f"File not found: {path}"
|
|
74
|
+
except IOError as e:
|
|
75
|
+
return None, f"Failed to read {path}: {e}"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def parse_ledger_verdicts(ledger_content: str) -> Dict[str, int]:
|
|
79
|
+
"""Parse OUTCOMES-LEDGER.md and count verdicts.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
ledger_content: Markdown table content
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Dict mapping verdict string (OK, FAILED, EMPTY, HUNG) to count.
|
|
86
|
+
"""
|
|
87
|
+
verdicts = {"OK": 0, "FAILED": 0, "EMPTY": 0, "HUNG": 0}
|
|
88
|
+
|
|
89
|
+
# Skip header lines (first 2)
|
|
90
|
+
lines = ledger_content.strip().split("\n")[2:]
|
|
91
|
+
|
|
92
|
+
for line in lines:
|
|
93
|
+
# Parse markdown table row: | col1 | col2 | ... |
|
|
94
|
+
if not line.startswith("|"):
|
|
95
|
+
continue
|
|
96
|
+
|
|
97
|
+
cells = [cell.strip() for cell in line.split("|")]
|
|
98
|
+
# Skip empty cells at start/end (from split)
|
|
99
|
+
cells = [c for c in cells if c]
|
|
100
|
+
|
|
101
|
+
# Verdict is typically at position 6 (0-indexed): ISO, agent_type, model, duration, tokens_in, tokens_out, verdict
|
|
102
|
+
if len(cells) >= 7:
|
|
103
|
+
verdict = cells[6]
|
|
104
|
+
if verdict in verdicts:
|
|
105
|
+
verdicts[verdict] += 1
|
|
106
|
+
|
|
107
|
+
return verdicts
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def format_defect_escape_section(data: Optional[Dict[str, Any]]) -> str:
|
|
111
|
+
"""Format defect escape telemetry section."""
|
|
112
|
+
if data is None:
|
|
113
|
+
return "## Defect Escape Telemetry\n\n*Not available*\n\n"
|
|
114
|
+
|
|
115
|
+
feature = data.get("feature_commits", 0)
|
|
116
|
+
fixforward = data.get("fixforward_commits", 0)
|
|
117
|
+
rate = data.get("fixforward_rate", 0)
|
|
118
|
+
first_try = data.get("first_try_estimate")
|
|
119
|
+
window = data.get("window", {})
|
|
120
|
+
since = window.get("since", "unknown")
|
|
121
|
+
|
|
122
|
+
section = f"""## Defect Escape Telemetry
|
|
123
|
+
|
|
124
|
+
**Window:** {since}
|
|
125
|
+
|
|
126
|
+
| Metric | Value |
|
|
127
|
+
|--------|-------|
|
|
128
|
+
| Feature commits | {feature} |
|
|
129
|
+
| Fix-forward commits | {fixforward} |
|
|
130
|
+
| Fix-forward rate | {rate:.2%} |
|
|
131
|
+
| First-try estimate | {first_try if first_try is not None else "N/A"}{f" ({first_try:.2%})" if first_try is not None else ""} |
|
|
132
|
+
|
|
133
|
+
"""
|
|
134
|
+
return section
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def format_mutation_test_section(data: Optional[Dict[str, Any]]) -> str:
|
|
138
|
+
"""Format mutation testing section."""
|
|
139
|
+
if data is None:
|
|
140
|
+
return "## Mutation Testing\n\n*Not available*\n\n"
|
|
141
|
+
|
|
142
|
+
killed = data.get("killed", 0)
|
|
143
|
+
survived = data.get("survived", 0)
|
|
144
|
+
total = killed + survived
|
|
145
|
+
survival_rate = (survived / total * 100) if total > 0 else 0
|
|
146
|
+
|
|
147
|
+
section = f"""## Mutation Testing
|
|
148
|
+
|
|
149
|
+
| Metric | Value |
|
|
150
|
+
|--------|-------|
|
|
151
|
+
| Mutations killed | {killed} |
|
|
152
|
+
| Mutations survived | {survived} |
|
|
153
|
+
| Total mutations | {total} |
|
|
154
|
+
| Survival rate (test gaps) | {survival_rate:.1f}% |
|
|
155
|
+
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
# List survived mutations if present
|
|
159
|
+
mutations = data.get("mutations", [])
|
|
160
|
+
if mutations:
|
|
161
|
+
section += "### Survived Mutations (Test Gaps)\n\n"
|
|
162
|
+
for mut in mutations:
|
|
163
|
+
line = mut.get("line", "?")
|
|
164
|
+
orig = mut.get("original", "?")
|
|
165
|
+
mutated = mut.get("mutated", "?")
|
|
166
|
+
section += f"- {line}: `{orig}` -> `{mutated}`\n"
|
|
167
|
+
section += "\n"
|
|
168
|
+
|
|
169
|
+
return section
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def format_claudemd_lint_section(data: Optional[Dict[str, Any]]) -> str:
|
|
173
|
+
"""Format CLAUDE.md lint findings section."""
|
|
174
|
+
if data is None:
|
|
175
|
+
return "## CLAUDE.md Lint\n\n*Not available*\n\n"
|
|
176
|
+
|
|
177
|
+
count = data.get("count", 0)
|
|
178
|
+
section = f"""## CLAUDE.md Lint
|
|
179
|
+
|
|
180
|
+
**Findings:** {count}
|
|
181
|
+
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
findings = data.get("findings", [])
|
|
185
|
+
if findings:
|
|
186
|
+
for finding in findings:
|
|
187
|
+
ftype = finding.get("type", "unknown")
|
|
188
|
+
msg = finding.get("message", "")
|
|
189
|
+
line = finding.get("line", "?")
|
|
190
|
+
section += f"- [{ftype}] {msg} (line {line})\n"
|
|
191
|
+
section += "\n"
|
|
192
|
+
else:
|
|
193
|
+
section += "*No issues found*\n\n"
|
|
194
|
+
|
|
195
|
+
return section
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def format_ledger_section(ledger_content: Optional[str]) -> str:
|
|
199
|
+
"""Format fleet ledger summary section."""
|
|
200
|
+
if ledger_content is None:
|
|
201
|
+
return "## Fleet Ledger Summary\n\n*Not available*\n\n"
|
|
202
|
+
|
|
203
|
+
verdicts = parse_ledger_verdicts(ledger_content)
|
|
204
|
+
section = """## Fleet Ledger Summary
|
|
205
|
+
|
|
206
|
+
| Verdict | Count |
|
|
207
|
+
|---------|-------|
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
total = sum(verdicts.values())
|
|
211
|
+
for verdict in ["OK", "FAILED", "EMPTY", "HUNG"]:
|
|
212
|
+
count = verdicts.get(verdict, 0)
|
|
213
|
+
section += f"| {verdict} | {count} |\n"
|
|
214
|
+
|
|
215
|
+
if total > 0:
|
|
216
|
+
ok_rate = verdicts["OK"] / total * 100
|
|
217
|
+
section += f"\n**Success rate:** {ok_rate:.1f}%\n\n"
|
|
218
|
+
else:
|
|
219
|
+
section += "\n*No ledger entries*\n\n"
|
|
220
|
+
|
|
221
|
+
return section
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def generate_report(
|
|
225
|
+
defect_escape_data: Optional[Dict[str, Any]],
|
|
226
|
+
mutation_test_data: Optional[Dict[str, Any]],
|
|
227
|
+
claudemd_lint_data: Optional[Dict[str, Any]],
|
|
228
|
+
ledger_content: Optional[str],
|
|
229
|
+
strict: bool = False,
|
|
230
|
+
timestamp: Optional[str] = None,
|
|
231
|
+
) -> Tuple[str, Optional[str]]:
|
|
232
|
+
"""Generate audit report from aggregated data.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
defect_escape_data: defect_escape.py JSON
|
|
236
|
+
mutation_test_data: mutation_test.py JSON
|
|
237
|
+
claudemd_lint_data: claudemd_lint.py JSON
|
|
238
|
+
ledger_content: OUTCOMES-LEDGER.md content
|
|
239
|
+
strict: If True, fail if any source is missing
|
|
240
|
+
timestamp: ISO timestamp for report (default: current UTC time)
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
Tuple of (report_markdown, error_msg). On success, error_msg is None.
|
|
244
|
+
"""
|
|
245
|
+
# In strict mode, all sources must be present
|
|
246
|
+
if strict:
|
|
247
|
+
missing = []
|
|
248
|
+
if defect_escape_data is None:
|
|
249
|
+
missing.append("defect-escape")
|
|
250
|
+
if mutation_test_data is None:
|
|
251
|
+
missing.append("mutation-test")
|
|
252
|
+
if claudemd_lint_data is None:
|
|
253
|
+
missing.append("claudemd-lint")
|
|
254
|
+
if ledger_content is None:
|
|
255
|
+
missing.append("ledger")
|
|
256
|
+
|
|
257
|
+
if missing:
|
|
258
|
+
return "", f"Strict mode: missing sources: {', '.join(missing)}"
|
|
259
|
+
|
|
260
|
+
# Build report
|
|
261
|
+
if timestamp is None:
|
|
262
|
+
timestamp = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
263
|
+
report = f"""# Audit Report
|
|
264
|
+
|
|
265
|
+
**Generated:** {timestamp}
|
|
266
|
+
|
|
267
|
+
"""
|
|
268
|
+
|
|
269
|
+
# Add each section
|
|
270
|
+
report += format_defect_escape_section(defect_escape_data)
|
|
271
|
+
report += format_mutation_test_section(mutation_test_data)
|
|
272
|
+
report += format_claudemd_lint_section(claudemd_lint_data)
|
|
273
|
+
report += format_ledger_section(ledger_content)
|
|
274
|
+
|
|
275
|
+
# Footer
|
|
276
|
+
report += f"""---
|
|
277
|
+
|
|
278
|
+
*Report generated by audit_report.py (deterministic, no external LLM calls)*
|
|
279
|
+
"""
|
|
280
|
+
|
|
281
|
+
return report, None
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def main():
|
|
285
|
+
"""Main entry point."""
|
|
286
|
+
parser = argparse.ArgumentParser(
|
|
287
|
+
description="Generate deterministic audit report from machine outputs"
|
|
288
|
+
)
|
|
289
|
+
parser.add_argument(
|
|
290
|
+
"--defect-escape",
|
|
291
|
+
type=str,
|
|
292
|
+
default=None,
|
|
293
|
+
help="Path to defect_escape.py JSON output",
|
|
294
|
+
)
|
|
295
|
+
parser.add_argument(
|
|
296
|
+
"--mutation-test",
|
|
297
|
+
type=str,
|
|
298
|
+
default=None,
|
|
299
|
+
help="Path to mutation_test.py JSON output",
|
|
300
|
+
)
|
|
301
|
+
parser.add_argument(
|
|
302
|
+
"--claudemd-lint",
|
|
303
|
+
type=str,
|
|
304
|
+
default=None,
|
|
305
|
+
help="Path to claudemd_lint.py JSON output",
|
|
306
|
+
)
|
|
307
|
+
parser.add_argument(
|
|
308
|
+
"--ledger",
|
|
309
|
+
type=str,
|
|
310
|
+
default=None,
|
|
311
|
+
help="Path to OUTCOMES-LEDGER.md file",
|
|
312
|
+
)
|
|
313
|
+
parser.add_argument(
|
|
314
|
+
"--out",
|
|
315
|
+
type=str,
|
|
316
|
+
default=None,
|
|
317
|
+
help="Write report to file (default: stdout)",
|
|
318
|
+
)
|
|
319
|
+
parser.add_argument(
|
|
320
|
+
"--strict",
|
|
321
|
+
action="store_true",
|
|
322
|
+
help="Fail if any source is missing",
|
|
323
|
+
)
|
|
324
|
+
parser.add_argument(
|
|
325
|
+
"--timestamp",
|
|
326
|
+
type=str,
|
|
327
|
+
default=None,
|
|
328
|
+
help="ISO timestamp for report (default: current UTC time)",
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
args = parser.parse_args()
|
|
332
|
+
|
|
333
|
+
# Load all sources
|
|
334
|
+
defect_escape_data, defect_err = load_json_file(args.defect_escape)
|
|
335
|
+
mutation_test_data, mutation_err = load_json_file(args.mutation_test)
|
|
336
|
+
claudemd_lint_data, claudemd_err = load_json_file(args.claudemd_lint)
|
|
337
|
+
ledger_content, ledger_err = load_ledger_file(args.ledger)
|
|
338
|
+
|
|
339
|
+
# Check for load errors
|
|
340
|
+
errors = []
|
|
341
|
+
if defect_err:
|
|
342
|
+
errors.append(f"Defect escape: {defect_err}")
|
|
343
|
+
if mutation_err:
|
|
344
|
+
errors.append(f"Mutation test: {mutation_err}")
|
|
345
|
+
if claudemd_err:
|
|
346
|
+
errors.append(f"CLAUDE.md lint: {claudemd_err}")
|
|
347
|
+
if ledger_err:
|
|
348
|
+
errors.append(f"Ledger: {ledger_err}")
|
|
349
|
+
|
|
350
|
+
# In non-strict mode, print warnings but continue
|
|
351
|
+
if errors and not args.strict:
|
|
352
|
+
for error in errors:
|
|
353
|
+
print(f"Warning: {error}", file=sys.stderr)
|
|
354
|
+
elif errors and args.strict:
|
|
355
|
+
for error in errors:
|
|
356
|
+
print(f"Error: {error}", file=sys.stderr)
|
|
357
|
+
sys.exit(1)
|
|
358
|
+
|
|
359
|
+
# Generate report
|
|
360
|
+
report, err = generate_report(
|
|
361
|
+
defect_escape_data,
|
|
362
|
+
mutation_test_data,
|
|
363
|
+
claudemd_lint_data,
|
|
364
|
+
ledger_content,
|
|
365
|
+
strict=args.strict,
|
|
366
|
+
timestamp=args.timestamp,
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
if err:
|
|
370
|
+
print(f"Error: {err}", file=sys.stderr)
|
|
371
|
+
sys.exit(1)
|
|
372
|
+
|
|
373
|
+
# Output report
|
|
374
|
+
if args.out:
|
|
375
|
+
try:
|
|
376
|
+
with open(args.out, "w", encoding="utf-8") as f:
|
|
377
|
+
f.write(report)
|
|
378
|
+
except IOError as e:
|
|
379
|
+
print(f"Error writing to {args.out}: {e}", file=sys.stderr)
|
|
380
|
+
sys.exit(1)
|
|
381
|
+
else:
|
|
382
|
+
print(report)
|
|
383
|
+
|
|
384
|
+
return 0
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
if __name__ == "__main__":
|
|
388
|
+
sys.exit(main())
|
package/tools/bench_runner.py
CHANGED
|
@@ -39,7 +39,10 @@ from __future__ import annotations
|
|
|
39
39
|
import argparse
|
|
40
40
|
import json
|
|
41
41
|
import re
|
|
42
|
+
import shutil
|
|
43
|
+
import subprocess
|
|
42
44
|
import sys
|
|
45
|
+
import time
|
|
43
46
|
from pathlib import Path
|
|
44
47
|
from typing import Callable, Dict, List, Optional, Tuple
|
|
45
48
|
|
|
@@ -346,10 +349,101 @@ def mock_runner(prompt: str) -> str:
|
|
|
346
349
|
return ""
|
|
347
350
|
|
|
348
351
|
|
|
352
|
+
# ---------------------------------------------------------------------------
|
|
353
|
+
# Claude CLI runner — shell the local `claude` command for real model calls.
|
|
354
|
+
# Gracefully unavailable if `claude` is not on PATH.
|
|
355
|
+
# ---------------------------------------------------------------------------
|
|
356
|
+
|
|
357
|
+
def _make_claude_runner(model_alias: str) -> ModelRunner:
|
|
358
|
+
"""Create a runner that shells the local `claude` CLI.
|
|
359
|
+
|
|
360
|
+
Args:
|
|
361
|
+
model_alias: A model alias (e.g., "haiku", "sonnet", "opus") or full
|
|
362
|
+
model id (e.g., "claude-haiku-4-5-20251001"). Passed to
|
|
363
|
+
claude --model <alias>.
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
A ModelRunner that shells `claude -p <prompt> --model <alias>
|
|
367
|
+
--output-format json` and returns (text, usage) with latency_ms.
|
|
368
|
+
Raises RuntimeError if claude is not available.
|
|
369
|
+
"""
|
|
370
|
+
def runner(prompt: str) -> tuple:
|
|
371
|
+
if not shutil.which("claude"):
|
|
372
|
+
raise RuntimeError(
|
|
373
|
+
"claude CLI not found on PATH: install it or check your PATH. "
|
|
374
|
+
"See https://github.com/anthropic-ai/claude-code for setup."
|
|
375
|
+
)
|
|
376
|
+
try:
|
|
377
|
+
start = time.time()
|
|
378
|
+
result = subprocess.run(
|
|
379
|
+
["claude", "-p", prompt, "--model", model_alias, "--output-format", "json"],
|
|
380
|
+
capture_output=True,
|
|
381
|
+
text=True,
|
|
382
|
+
timeout=300, # 5 minutes per task
|
|
383
|
+
)
|
|
384
|
+
elapsed_ms = (time.time() - start) * 1000
|
|
385
|
+
if result.returncode != 0:
|
|
386
|
+
raise RuntimeError(
|
|
387
|
+
f"claude CLI exited {result.returncode}: {result.stderr}"
|
|
388
|
+
)
|
|
389
|
+
# Parse JSON output
|
|
390
|
+
output_json = json.loads(result.stdout)
|
|
391
|
+
# Extract text and tokens from the JSON response
|
|
392
|
+
# The claude CLI returns a structure like:
|
|
393
|
+
# {"type": "result", "subtype": "success", "result": "...", "usage": {...}}
|
|
394
|
+
text = None
|
|
395
|
+
tokens = None
|
|
396
|
+
if isinstance(output_json, dict):
|
|
397
|
+
# Try new format first (--output-format json with --print)
|
|
398
|
+
if "result" in output_json:
|
|
399
|
+
text = output_json.get("result")
|
|
400
|
+
# Fallback to old message format if needed
|
|
401
|
+
elif "content" in output_json:
|
|
402
|
+
content = output_json.get("content")
|
|
403
|
+
if isinstance(content, list) and content:
|
|
404
|
+
first = content[0]
|
|
405
|
+
if isinstance(first, dict):
|
|
406
|
+
text = first.get("text")
|
|
407
|
+
# Extract token count if available
|
|
408
|
+
usage_obj = output_json.get("usage", {})
|
|
409
|
+
if isinstance(usage_obj, dict):
|
|
410
|
+
tokens = usage_obj.get("output_tokens")
|
|
411
|
+
if not text:
|
|
412
|
+
raise RuntimeError(
|
|
413
|
+
f"unable to extract text from claude CLI JSON response: {output_json}"
|
|
414
|
+
)
|
|
415
|
+
usage = {
|
|
416
|
+
"tokens": tokens,
|
|
417
|
+
"latency_ms": elapsed_ms,
|
|
418
|
+
}
|
|
419
|
+
return (text, usage)
|
|
420
|
+
except subprocess.TimeoutExpired:
|
|
421
|
+
raise RuntimeError(f"claude CLI timed out after 300s")
|
|
422
|
+
except json.JSONDecodeError as e:
|
|
423
|
+
raise RuntimeError(f"claude CLI output is not valid JSON: {e}")
|
|
424
|
+
except Exception as e:
|
|
425
|
+
raise RuntimeError(f"claude CLI error: {e}")
|
|
426
|
+
|
|
427
|
+
return runner
|
|
428
|
+
|
|
429
|
+
|
|
349
430
|
RUNNERS: Dict[str, ModelRunner] = {
|
|
350
431
|
"mock": mock_runner,
|
|
351
432
|
}
|
|
352
433
|
|
|
434
|
+
# Register claude runners if claude CLI is available
|
|
435
|
+
_CLAUDE_AVAILABLE = bool(shutil.which("claude"))
|
|
436
|
+
if _CLAUDE_AVAILABLE:
|
|
437
|
+
for alias in ("haiku", "sonnet", "opus"):
|
|
438
|
+
try:
|
|
439
|
+
# Pre-flight check: make sure the runner can be created
|
|
440
|
+
# (we don't call it until later, but we want to register it)
|
|
441
|
+
runner = _make_claude_runner(alias)
|
|
442
|
+
RUNNERS[alias] = runner
|
|
443
|
+
except Exception:
|
|
444
|
+
# If something fails during registration, skip this model
|
|
445
|
+
pass
|
|
446
|
+
|
|
353
447
|
|
|
354
448
|
def _fmt(value, suffix: str = "") -> str:
|
|
355
449
|
return "-" if value is None else f"{value:g}{suffix}"
|
|
@@ -415,11 +509,14 @@ def print_comparison(summaries: List[dict], stream=None) -> None:
|
|
|
415
509
|
|
|
416
510
|
def main(argv: Optional[List[str]] = None) -> int:
|
|
417
511
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
512
|
+
available_runners = sorted(RUNNERS.keys())
|
|
513
|
+
default_runner = "mock"
|
|
418
514
|
parser.add_argument(
|
|
419
515
|
"--runner",
|
|
420
|
-
default=
|
|
421
|
-
choices=
|
|
422
|
-
help="Which registered model runner to score (default:
|
|
516
|
+
default=default_runner,
|
|
517
|
+
choices=available_runners,
|
|
518
|
+
help=f"Which registered model runner to score (default: {default_runner}, offline/zero-cost). "
|
|
519
|
+
f"Available: {', '.join(available_runners)}",
|
|
423
520
|
)
|
|
424
521
|
parser.add_argument("--tasks", default=None, help="Override path to tasks.jsonl")
|
|
425
522
|
parser.add_argument("--ground-truth", default=None, help="Override path to ground_truth.jsonl")
|