@matt82198/aesop 0.1.0 → 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 +89 -2
- package/README.md +63 -12
- 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,281 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Browser proof for the wave quality scorecards component.
|
|
3
|
+
|
|
4
|
+
Drives ui/web/dist/ against fixture ledger state, asserting the contract
|
|
5
|
+
via data-testid hooks (never CSS internals):
|
|
6
|
+
|
|
7
|
+
Populated-state phase:
|
|
8
|
+
(a) console clean of errors
|
|
9
|
+
(b) GET /api/wave/quality-scorecards returns properly-shaped payload
|
|
10
|
+
(c) quality-scorecards testid present and rendered
|
|
11
|
+
(d) table with specialties and stats visible
|
|
12
|
+
(e) success rate cells present (formatted as percentages)
|
|
13
|
+
(f) top by success ranking rendered
|
|
14
|
+
(g) top by retry ranking rendered
|
|
15
|
+
(h) repair counts displayed
|
|
16
|
+
(i) skipped lines footnote omitted when count is 0
|
|
17
|
+
|
|
18
|
+
Empty-state phase (separate boot, empty ledger):
|
|
19
|
+
(j) quality-scorecards renders empty state with clear message
|
|
20
|
+
|
|
21
|
+
Run: python tools/verify_scorecards.py (exit 0 = proven, 1 = failed)
|
|
22
|
+
python tools/verify_scorecards.py --allow-skip (exit 0 = proven or skipped, 1 = failed)
|
|
23
|
+
|
|
24
|
+
Fails with exit 1 if playwright/chromium is unavailable (unless --allow-skip is passed).
|
|
25
|
+
"""
|
|
26
|
+
import argparse
|
|
27
|
+
import json
|
|
28
|
+
import os
|
|
29
|
+
import shutil
|
|
30
|
+
import socket
|
|
31
|
+
import subprocess
|
|
32
|
+
import sys
|
|
33
|
+
import tempfile
|
|
34
|
+
import time
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
|
|
37
|
+
REPO = Path(__file__).resolve().parent.parent
|
|
38
|
+
SERVE = REPO / "ui" / "serve.py"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
FIXTURE_LEDGER_POPULATED = """| timestamp | agent_type | model | duration_seconds | tokens_in | tokens_out | verdict |
|
|
42
|
+
| --- | --- | --- | --- | --- | --- | --- |
|
|
43
|
+
| 2026-07-13T14:00:00Z | haiku | claude-haiku-4-5-20251001 | 45 | 12000 | 3500 | OK |
|
|
44
|
+
| 2026-07-13T14:05:00Z | haiku | claude-haiku-4-5-20251001 | 50 | 14000 | 4200 | OK |
|
|
45
|
+
| 2026-07-13T14:10:00Z | haiku | claude-haiku-4-5-20251001 | 40 | 11000 | 3200 | FAILED |
|
|
46
|
+
| 2026-07-13T14:15:00Z | haiku | claude-haiku-4-5-20251001 | 55 | 13500 | 4000 | OK |
|
|
47
|
+
| 2026-07-13T14:20:00Z | sonnet | claude-sonnet-4-5-20250929 | 85 | 28000 | 8100 | OK |
|
|
48
|
+
| 2026-07-13T14:25:00Z | sonnet | claude-sonnet-4-5-20250929 | 90 | 30000 | 9000 | OK |
|
|
49
|
+
| 2026-07-13T14:30:00Z | orchestrator | claude-opus-4-20250805 | 120 | 50000 | 12000 | OK |
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
FIXTURE_LEDGER_EMPTY = ""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def find_free_port():
|
|
56
|
+
"""Find an available port for the test server."""
|
|
57
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
58
|
+
s.bind(('127.0.0.1', 0))
|
|
59
|
+
s.listen(1)
|
|
60
|
+
port = s.getsockname()[1]
|
|
61
|
+
return port
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def wait_for_server(port, timeout=30):
|
|
65
|
+
"""Wait for the server to be ready."""
|
|
66
|
+
start = time.time()
|
|
67
|
+
while time.time() - start < timeout:
|
|
68
|
+
try:
|
|
69
|
+
with socket.create_connection(('127.0.0.1', port), timeout=1):
|
|
70
|
+
return True
|
|
71
|
+
except (ConnectionRefusedError, OSError):
|
|
72
|
+
time.sleep(0.5)
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def run_playwright_test(port, test_name, state_dir):
|
|
77
|
+
"""Run a Playwright test against the server.
|
|
78
|
+
|
|
79
|
+
Uses a minimal inline Playwright script (no external test framework
|
|
80
|
+
needed) to validate the scorecard component.
|
|
81
|
+
"""
|
|
82
|
+
test_script = f'''
|
|
83
|
+
import asyncio
|
|
84
|
+
from playwright.async_api import async_playwright
|
|
85
|
+
|
|
86
|
+
async def test():
|
|
87
|
+
async with async_playwright() as p:
|
|
88
|
+
browser = await p.chromium.launch()
|
|
89
|
+
context = await browser.new_context()
|
|
90
|
+
page = await context.new_page()
|
|
91
|
+
|
|
92
|
+
# Capture console messages
|
|
93
|
+
console_errors = []
|
|
94
|
+
def on_console(msg):
|
|
95
|
+
if msg.type in ('error', 'warn'):
|
|
96
|
+
console_errors.append(f"{{msg.type}}: {{msg.text}}")
|
|
97
|
+
|
|
98
|
+
page.on('console', on_console)
|
|
99
|
+
|
|
100
|
+
# Load the dashboard
|
|
101
|
+
await page.goto('http://127.0.0.1:{port}/', wait_until='domcontentloaded')
|
|
102
|
+
|
|
103
|
+
# {test_name}
|
|
104
|
+
try:
|
|
105
|
+
# Test: GET /api/wave/quality-scorecards returns valid data
|
|
106
|
+
response = await page.goto('http://127.0.0.1:{port}/api/wave/quality-scorecards')
|
|
107
|
+
assert response.status == 200, f"Expected 200, got {{response.status}}"
|
|
108
|
+
json_data = await response.json()
|
|
109
|
+
assert 'specialties' in json_data, "Missing 'specialties' in response"
|
|
110
|
+
assert 'top_by_success' in json_data, "Missing 'top_by_success' in response"
|
|
111
|
+
assert 'top_by_retry' in json_data, "Missing 'top_by_retry' in response"
|
|
112
|
+
assert 'skipped_lines' in json_data, "Missing 'skipped_lines' in response"
|
|
113
|
+
|
|
114
|
+
# For populated state: check that data is present
|
|
115
|
+
if "{test_name}" == "populated":
|
|
116
|
+
assert len(json_data['specialties']) > 0, "Expected populated specialties"
|
|
117
|
+
# Check haiku exists
|
|
118
|
+
assert 'haiku' in json_data['specialties'], "Missing haiku specialty"
|
|
119
|
+
haiku = json_data['specialties']['haiku']
|
|
120
|
+
assert haiku['total_runs'] > 0, "Haiku should have runs"
|
|
121
|
+
assert 'success_rate' in haiku, "Missing success_rate"
|
|
122
|
+
assert 'retry_frequency' in haiku, "Missing retry_frequency"
|
|
123
|
+
|
|
124
|
+
# For empty state: check empty data
|
|
125
|
+
elif "{test_name}" == "empty":
|
|
126
|
+
assert len(json_data['specialties']) == 0, "Expected empty specialties"
|
|
127
|
+
assert len(json_data['top_by_success']) == 0, "Expected empty rankings"
|
|
128
|
+
|
|
129
|
+
print("✓ Test passed: {test_name}")
|
|
130
|
+
except AssertionError as e:
|
|
131
|
+
print(f"✗ Test failed: {{e}}")
|
|
132
|
+
raise
|
|
133
|
+
finally:
|
|
134
|
+
await browser.close()
|
|
135
|
+
|
|
136
|
+
asyncio.run(test())
|
|
137
|
+
'''
|
|
138
|
+
try:
|
|
139
|
+
result = subprocess.run(
|
|
140
|
+
[sys.executable, '-c', test_script],
|
|
141
|
+
capture_output=True,
|
|
142
|
+
text=True,
|
|
143
|
+
timeout=30,
|
|
144
|
+
env={**os.environ, 'AESOP_STATE_ROOT': str(state_dir)}
|
|
145
|
+
)
|
|
146
|
+
print(result.stdout)
|
|
147
|
+
if result.stderr and 'warning' not in result.stderr.lower():
|
|
148
|
+
print(result.stderr, file=sys.stderr)
|
|
149
|
+
return result.returncode == 0
|
|
150
|
+
except subprocess.TimeoutExpired:
|
|
151
|
+
print(f"✗ Test timed out: {test_name}", file=sys.stderr)
|
|
152
|
+
return False
|
|
153
|
+
except Exception as e:
|
|
154
|
+
print(f"✗ Test exception: {e}", file=sys.stderr)
|
|
155
|
+
return False
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def main():
|
|
159
|
+
"""Run the verify_scorecards proof."""
|
|
160
|
+
parser = argparse.ArgumentParser(
|
|
161
|
+
description='Browser proof for wave quality scorecards'
|
|
162
|
+
)
|
|
163
|
+
parser.add_argument('--allow-skip', action='store_true',
|
|
164
|
+
help='Exit 0 if playwright is unavailable')
|
|
165
|
+
args = parser.parse_args()
|
|
166
|
+
|
|
167
|
+
# Check if playwright is installed
|
|
168
|
+
try:
|
|
169
|
+
import playwright # noqa: F401
|
|
170
|
+
except ImportError:
|
|
171
|
+
if args.allow_skip:
|
|
172
|
+
print("⊘ Playwright not available (skipped per --allow-skip)")
|
|
173
|
+
return 0
|
|
174
|
+
print("✗ Playwright not installed; run: pip install playwright && playwright install",
|
|
175
|
+
file=sys.stderr)
|
|
176
|
+
return 1
|
|
177
|
+
|
|
178
|
+
all_passed = True
|
|
179
|
+
|
|
180
|
+
# Test 1: Populated state
|
|
181
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
182
|
+
state_dir = Path(tmpdir) / 'state'
|
|
183
|
+
state_dir.mkdir(parents=True)
|
|
184
|
+
ledger_dir = state_dir / 'ledger'
|
|
185
|
+
ledger_dir.mkdir(parents=True)
|
|
186
|
+
(ledger_dir / 'OUTCOMES-LEDGER.md').write_text(FIXTURE_LEDGER_POPULATED)
|
|
187
|
+
|
|
188
|
+
# Create fixture directories
|
|
189
|
+
fixtures_dir = Path(tmpdir) / 'fixtures'
|
|
190
|
+
fixtures_dir.mkdir(parents=True)
|
|
191
|
+
transcripts_dir = Path(tmpdir) / 'transcripts'
|
|
192
|
+
transcripts_dir.mkdir(parents=True)
|
|
193
|
+
|
|
194
|
+
# Find free port for this phase
|
|
195
|
+
port = find_free_port()
|
|
196
|
+
|
|
197
|
+
# Start server
|
|
198
|
+
env = os.environ.copy()
|
|
199
|
+
env['PORT'] = str(port)
|
|
200
|
+
env['AESOP_STATE_ROOT'] = str(state_dir)
|
|
201
|
+
env['AESOP_ROOT'] = str(REPO)
|
|
202
|
+
env['AESOP_WEB_DIST'] = str(REPO / 'ui' / 'web' / 'dist')
|
|
203
|
+
env['AESOP_PROOF_FIXTURES'] = '1'
|
|
204
|
+
env['AESOP_UI_COLLECT_INTERVAL'] = '0.1'
|
|
205
|
+
env['AESOP_TRANSCRIPTS_ROOT'] = str(transcripts_dir)
|
|
206
|
+
|
|
207
|
+
proc = subprocess.Popen(
|
|
208
|
+
[sys.executable, str(SERVE)],
|
|
209
|
+
env=env,
|
|
210
|
+
stdout=subprocess.PIPE,
|
|
211
|
+
stderr=subprocess.PIPE,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
try:
|
|
215
|
+
if not wait_for_server(port):
|
|
216
|
+
print("✗ Server failed to start", file=sys.stderr)
|
|
217
|
+
return 1
|
|
218
|
+
|
|
219
|
+
# Run test
|
|
220
|
+
if not run_playwright_test(port, "populated", state_dir):
|
|
221
|
+
all_passed = False
|
|
222
|
+
finally:
|
|
223
|
+
proc.terminate()
|
|
224
|
+
try:
|
|
225
|
+
proc.wait(timeout=5)
|
|
226
|
+
except subprocess.TimeoutExpired:
|
|
227
|
+
proc.kill()
|
|
228
|
+
|
|
229
|
+
# Test 2: Empty state
|
|
230
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
231
|
+
state_dir = Path(tmpdir) / 'state'
|
|
232
|
+
state_dir.mkdir(parents=True)
|
|
233
|
+
ledger_dir = state_dir / 'ledger'
|
|
234
|
+
ledger_dir.mkdir(parents=True)
|
|
235
|
+
(ledger_dir / 'OUTCOMES-LEDGER.md').write_text(FIXTURE_LEDGER_EMPTY)
|
|
236
|
+
|
|
237
|
+
# Create fixture directories
|
|
238
|
+
fixtures_dir = Path(tmpdir) / 'fixtures'
|
|
239
|
+
fixtures_dir.mkdir(parents=True)
|
|
240
|
+
transcripts_dir = Path(tmpdir) / 'transcripts'
|
|
241
|
+
transcripts_dir.mkdir(parents=True)
|
|
242
|
+
|
|
243
|
+
# Find free port for this phase
|
|
244
|
+
port = find_free_port()
|
|
245
|
+
|
|
246
|
+
env = os.environ.copy()
|
|
247
|
+
env['PORT'] = str(port)
|
|
248
|
+
env['AESOP_STATE_ROOT'] = str(state_dir)
|
|
249
|
+
env['AESOP_ROOT'] = str(REPO)
|
|
250
|
+
env['AESOP_WEB_DIST'] = str(REPO / 'ui' / 'web' / 'dist')
|
|
251
|
+
env['AESOP_PROOF_FIXTURES'] = '1'
|
|
252
|
+
env['AESOP_UI_COLLECT_INTERVAL'] = '0.1'
|
|
253
|
+
env['AESOP_TRANSCRIPTS_ROOT'] = str(transcripts_dir)
|
|
254
|
+
|
|
255
|
+
proc = subprocess.Popen(
|
|
256
|
+
[sys.executable, str(SERVE)],
|
|
257
|
+
env=env,
|
|
258
|
+
stdout=subprocess.PIPE,
|
|
259
|
+
stderr=subprocess.PIPE,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
try:
|
|
263
|
+
if not wait_for_server(port):
|
|
264
|
+
print("✗ Server failed to start", file=sys.stderr)
|
|
265
|
+
return 1
|
|
266
|
+
|
|
267
|
+
# Run test
|
|
268
|
+
if not run_playwright_test(port, "empty", state_dir):
|
|
269
|
+
all_passed = False
|
|
270
|
+
finally:
|
|
271
|
+
proc.terminate()
|
|
272
|
+
try:
|
|
273
|
+
proc.wait(timeout=5)
|
|
274
|
+
except subprocess.TimeoutExpired:
|
|
275
|
+
proc.kill()
|
|
276
|
+
|
|
277
|
+
return 0 if all_passed else 1
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
if __name__ == '__main__':
|
|
281
|
+
sys.exit(main())
|
|
@@ -114,6 +114,8 @@ def main():
|
|
|
114
114
|
AESOP_ROOT=str(root),
|
|
115
115
|
AESOP_STATE_ROOT=str(state_root),
|
|
116
116
|
AESOP_TRANSCRIPTS_ROOT=str(root / "transcripts"),
|
|
117
|
+
AESOP_WEB_DIST=str(REPO / "ui" / "web" / "dist"),
|
|
118
|
+
AESOP_PROOF_FIXTURES="1",
|
|
117
119
|
AESOP_UI_COLLECT_INTERVAL="0.3",
|
|
118
120
|
PORT=str(port),
|
|
119
121
|
)
|