@matt82198/aesop 0.1.0-rc.1 → 0.2.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/CHANGELOG.md +101 -2
- package/README.md +66 -15
- package/bin/cli.js +164 -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/INSTALL.md +70 -1
- package/docs/QUICKSTART.md +80 -0
- package/docs/README.md +33 -3
- package/docs/TEAM-STATE.md +540 -0
- package/driver/CLAUDE.md +148 -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 +626 -0
- package/driver/openai_compatible_driver.py +249 -0
- package/driver/openai_transport.py +146 -0
- package/driver/verification_policy.py +75 -0
- package/driver/wave_bridge.py +246 -0
- package/driver/wave_loop.py +1041 -0
- package/hooks/pre-push-policy.sh +109 -28
- package/mcp/server.mjs +320 -4
- package/package.json +23 -15
- 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 +2 -1
- package/state_store/api.py +19 -4
- package/state_store/coordination.py +209 -0
- package/state_store/identity.py +51 -0
- package/state_store/store.py +185 -73
- 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 +63 -31
- package/tools/cost_econ.py +480 -0
- package/tools/defect_escape.py +252 -0
- package/tools/doctor.js +1 -1
- package/tools/eod_sweep.py +58 -0
- 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 +401 -0
- package/tools/portability_check.py +206 -0
- package/tools/reconcile.py +7 -4
- package/tools/secret_scan.py +137 -50
- package/tools/self_stats.py +20 -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_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 +512 -0
- package/tools/wave_resume.py +215 -0
- package/tools/wave_templates.py +215 -0
- package/ui/agents.py +68 -14
- package/ui/collectors.py +0 -55
- package/ui/config.py +7 -2
- package/ui/cost.py +231 -12
- package/ui/handler.py +183 -0
- package/ui/quality_scorecard.py +232 -0
- 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 +377 -0
- package/ui/web/dist/assets/index-BGbgw2Nh.js +9 -0
- package/ui/web/dist/assets/index-DqZLgwNg.css +1 -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,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Reusable Library",
|
|
3
|
+
"description": "Bootstrap a reusable library wave with core functionality, comprehensive tests, and documentation",
|
|
4
|
+
"categories": ["library", "sdk", "tooling"],
|
|
5
|
+
"items": [
|
|
6
|
+
{
|
|
7
|
+
"slug": "core-module",
|
|
8
|
+
"ownsFiles": [
|
|
9
|
+
"src/{project_name}/__init__.py",
|
|
10
|
+
"src/{project_name}/core.py",
|
|
11
|
+
"src/{project_name}/utils.py",
|
|
12
|
+
"src/{project_name}/types.py"
|
|
13
|
+
],
|
|
14
|
+
"prompt": "Build the core module for the {project_name} library. Implement the primary API/functions with clear interfaces. Export public functions in __init__.py. Include type hints using Python typing module. Add docstrings to all public functions (Google/NumPy style). Ensure the module is importable and has zero external dependencies (only stdlib or pinned minimal deps).",
|
|
15
|
+
"testCmd": "python -c \"import {project_name}; print(dir({project_name}))\" && python -m pytest tests/unit/ -v",
|
|
16
|
+
"workDir": "{base_dir}"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"slug": "comprehensive-tests",
|
|
20
|
+
"ownsFiles": [
|
|
21
|
+
"tests/unit/",
|
|
22
|
+
"tests/integration/",
|
|
23
|
+
"tests/conftest.py",
|
|
24
|
+
"tests/fixtures/",
|
|
25
|
+
"coverage.xml",
|
|
26
|
+
".coveragerc",
|
|
27
|
+
"tests/README.md"
|
|
28
|
+
],
|
|
29
|
+
"prompt": "Write comprehensive test suite for {project_name}. Create unit tests for all core functions (happy path, edge cases, error conditions). Write integration tests for end-to-end flows. Aim for >90% code coverage. Use pytest framework with fixtures for reusable test data. Include performance benchmarks. Generate coverage reports in XML and HTML formats. Document test patterns.",
|
|
30
|
+
"testCmd": "python -m pytest tests/ -v --cov={project_name} --cov-report=term-missing --cov-report=html",
|
|
31
|
+
"workDir": "{base_dir}"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"slug": "documentation",
|
|
35
|
+
"ownsFiles": [
|
|
36
|
+
"docs/",
|
|
37
|
+
"README.md",
|
|
38
|
+
"docs/conf.py",
|
|
39
|
+
"docs/source/",
|
|
40
|
+
"docs/Makefile",
|
|
41
|
+
"CONTRIBUTING.md",
|
|
42
|
+
"CHANGELOG.md",
|
|
43
|
+
"LICENSE"
|
|
44
|
+
],
|
|
45
|
+
"prompt": "Create comprehensive documentation for {project_name}. Write a clear README with quick-start examples. Build Sphinx docs with: getting-started guide, API reference (auto-generated from docstrings), design docs, troubleshooting. Add contribution guidelines and changelog. Include code examples. Make docs buildable locally (HTML/PDF). Add documentation to the build CI pipeline.",
|
|
46
|
+
"testCmd": "cd docs && sphinx-build -W -b html source _build/html && ls -la _build/html/index.html",
|
|
47
|
+
"workDir": "{base_dir}"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"slug": "packaging-release",
|
|
51
|
+
"ownsFiles": [
|
|
52
|
+
"pyproject.toml",
|
|
53
|
+
"setup.cfg",
|
|
54
|
+
"setup.py",
|
|
55
|
+
"MANIFEST.in",
|
|
56
|
+
".github/workflows/publish.yml",
|
|
57
|
+
"PKG-INFO",
|
|
58
|
+
"VERSION"
|
|
59
|
+
],
|
|
60
|
+
"prompt": "Set up packaging and release infrastructure for {project_name}. Configure pyproject.toml (PEP 517/518) with metadata, dependencies, and entry points. Create GitHub Actions workflow for publishing to PyPI (semantic versioning, release tags). Write instructions for local testing with pip install -e. Include security scanning (audit for vulnerabilities). Ensure the package is installable and validated before release.",
|
|
61
|
+
"testCmd": "python -m build --sdist --wheel . && python -m twine check dist/*",
|
|
62
|
+
"workDir": "{base_dir}"
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "SaaS Web Application",
|
|
3
|
+
"description": "Bootstrap a SaaS application wave with backend API, frontend, and infrastructure components",
|
|
4
|
+
"categories": ["backend", "frontend", "infrastructure"],
|
|
5
|
+
"items": [
|
|
6
|
+
{
|
|
7
|
+
"slug": "backend-api",
|
|
8
|
+
"ownsFiles": [
|
|
9
|
+
"src/api/main.py",
|
|
10
|
+
"src/api/models.py",
|
|
11
|
+
"src/api/routes/",
|
|
12
|
+
"tests/api/",
|
|
13
|
+
"requirements.txt",
|
|
14
|
+
"pyproject.toml"
|
|
15
|
+
],
|
|
16
|
+
"prompt": "Build the {project_name} backend API service. Create a FastAPI/Flask application with basic CRUD routes, database models (users, products), and authentication scaffolding. Implement endpoints for /api/users, /api/products. Include database connection pooling and schema migrations. Tests must cover happy path and error cases. Use {base_dir}/src/api as the root.",
|
|
17
|
+
"testCmd": "python -m pytest tests/api/ -v",
|
|
18
|
+
"workDir": "{base_dir}"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"slug": "frontend-web",
|
|
22
|
+
"ownsFiles": [
|
|
23
|
+
"src/web/index.html",
|
|
24
|
+
"src/web/styles.css",
|
|
25
|
+
"src/web/components/",
|
|
26
|
+
"src/web/pages/",
|
|
27
|
+
"src/web/lib/",
|
|
28
|
+
"tests/web/",
|
|
29
|
+
"package.json"
|
|
30
|
+
],
|
|
31
|
+
"prompt": "Build the {project_name} web frontend. Create a React or vanilla-JS SPA with pages for dashboard, user management, and product catalog. Implement client-side routing, form validation, and API client library. Use {base_dir}/src/web as root. Ensure responsive design and accessibility (WCAG AA).",
|
|
32
|
+
"testCmd": "npm test -- --coverage",
|
|
33
|
+
"workDir": "{base_dir}"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"slug": "infrastructure-deploy",
|
|
37
|
+
"ownsFiles": [
|
|
38
|
+
"infra/docker-compose.yml",
|
|
39
|
+
"infra/Dockerfile.api",
|
|
40
|
+
"infra/Dockerfile.web",
|
|
41
|
+
"infra/kubernetes/",
|
|
42
|
+
"infra/terraform/",
|
|
43
|
+
".github/workflows/deploy.yml",
|
|
44
|
+
"infra/README.md"
|
|
45
|
+
],
|
|
46
|
+
"prompt": "Set up {project_name} deployment infrastructure. Create Docker images for both API and web services, write a docker-compose.yml for local development, and scaffold Kubernetes manifests (deployment, service, ingress) for production. Add a GitHub Actions workflow for CI/CD. Include health checks and resource limits.",
|
|
47
|
+
"testCmd": "docker-compose config > /dev/null && ls -la infra/kubernetes/*.yaml",
|
|
48
|
+
"workDir": "{base_dir}"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"slug": "database-schema",
|
|
52
|
+
"ownsFiles": [
|
|
53
|
+
"migrations/",
|
|
54
|
+
"src/api/database.py",
|
|
55
|
+
"schema.sql",
|
|
56
|
+
"seeds/initial.sql",
|
|
57
|
+
"tests/database/"
|
|
58
|
+
],
|
|
59
|
+
"prompt": "Design and implement the {project_name} database schema. Create SQL migrations for core tables: users (id, email, created_at), products (id, name, price), and orders (id, user_id, total). Add indexes on foreign keys and email. Provide seed data for development. Write Python ORM models (SQLAlchemy) that correspond to tables.",
|
|
60
|
+
"testCmd": "python -m pytest tests/database/ -v --tb=short",
|
|
61
|
+
"workDir": "{base_dir}"
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
}
|
|
@@ -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")
|