@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
package/ui/cost.py
CHANGED
|
@@ -5,9 +5,14 @@ This module provides get_cost_summary() which parses the outcomes ledger
|
|
|
5
5
|
markdown table and returns per-model, per-day, and overall cost/token aggregations
|
|
6
6
|
with optional pricing estimates.
|
|
7
7
|
|
|
8
|
-
Ledger format (markdown table):
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
Ledger format (markdown table, supports both 7-column and 9-column):
|
|
9
|
+
Legacy 7-column:
|
|
10
|
+
| ISO timestamp | agent_type | model | duration | tokens_in | tokens_out | verdict |
|
|
11
|
+
| 2026-07-11T22:08:17 | Agent | claude-haiku-4-5-20251001 | 0 | 8 | 186 | OK |
|
|
12
|
+
|
|
13
|
+
Extended 9-column (phase/wave optional):
|
|
14
|
+
| ISO timestamp | agent_type | model | duration | tokens_in | tokens_out | verdict | phase | wave |
|
|
15
|
+
| 2026-07-11T22:08:17 | Agent | claude-haiku-4-5-20251001 | 0 | 8 | 186 | OK | build | 7 |
|
|
11
16
|
|
|
12
17
|
CostSummary JSON shape (returned by get_cost_summary()):
|
|
13
18
|
{
|
|
@@ -69,10 +74,11 @@ def _validate_ledger_format(lines):
|
|
|
69
74
|
Checks the first non-empty, non-separator, non-header line to ensure it looks like a data row
|
|
70
75
|
(has ISO timestamp, agent type, model, numeric fields, verdict). Returns (is_valid, error_message).
|
|
71
76
|
|
|
72
|
-
|
|
73
|
-
|
|
|
74
|
-
|
|
75
|
-
|
|
77
|
+
Accepts two formats:
|
|
78
|
+
- Legacy 7-column: | timestamp | agent_type | model | duration | tokens_in | tokens_out | verdict |
|
|
79
|
+
- Extended 9-column: | timestamp | agent_type | model | duration | tokens_in | tokens_out | verdict | phase | wave |
|
|
80
|
+
|
|
81
|
+
Optional trailing columns (phase, wave) are ignored during validation.
|
|
76
82
|
"""
|
|
77
83
|
for line in lines:
|
|
78
84
|
line = line.strip()
|
|
@@ -91,11 +97,13 @@ def _validate_ledger_format(lines):
|
|
|
91
97
|
|
|
92
98
|
parts = [p.strip() for p in line.split('|')]
|
|
93
99
|
|
|
94
|
-
#
|
|
95
|
-
|
|
96
|
-
|
|
100
|
+
# Accept both 7-column (9 parts) and 9-column (11 parts) formats
|
|
101
|
+
# 7 columns: ['', col1, col2, col3, col4, col5, col6, col7, '']
|
|
102
|
+
# 9 columns: ['', col1, col2, col3, col4, col5, col6, col7, col8, col9, '']
|
|
103
|
+
if len(parts) < 9:
|
|
104
|
+
return False, f"Too few columns, got {len(parts) - 2} (expected at least 7)"
|
|
97
105
|
|
|
98
|
-
# Extract columns
|
|
106
|
+
# Extract core columns (first 7 required columns)
|
|
99
107
|
try:
|
|
100
108
|
timestamp = parts[1]
|
|
101
109
|
agent_type = parts[2]
|
|
@@ -145,11 +153,18 @@ def get_cost_summary():
|
|
|
145
153
|
All config paths are read at call time (not import time) to ensure
|
|
146
154
|
test-fixture isolation via config.reload().
|
|
147
155
|
|
|
156
|
+
Extended fields (additively):
|
|
157
|
+
- per_week_costs: dict of "YYYY-Www" -> week cost/token totals and model mix
|
|
158
|
+
- verdict_weighted_cost: cost-per-outcome metrics (cost per OK, weighted by verdict distribution)
|
|
159
|
+
- model_mix_trend: per-day model usage distribution (%)
|
|
160
|
+
|
|
148
161
|
Returns:
|
|
149
162
|
dict: CostSummary with models, daily_totals, overall_scorecard,
|
|
150
|
-
skipped_lines, has_pricing, estimates_by_model
|
|
163
|
+
skipped_lines, has_pricing, estimates_by_model, per_week_costs,
|
|
164
|
+
verdict_weighted_cost, model_mix_trend (or error field if invalid).
|
|
151
165
|
"""
|
|
152
166
|
import sys
|
|
167
|
+
from datetime import datetime, timedelta
|
|
153
168
|
|
|
154
169
|
# Read ledger path at call time
|
|
155
170
|
ledger_file = config.STATE_DIR / "ledger" / "OUTCOMES-LEDGER.md"
|
|
@@ -172,6 +187,14 @@ def get_cost_summary():
|
|
|
172
187
|
"skipped_lines": 0,
|
|
173
188
|
"has_pricing": False,
|
|
174
189
|
"estimates_by_model": {},
|
|
190
|
+
"per_week_costs": {},
|
|
191
|
+
"verdict_weighted_cost": {
|
|
192
|
+
"cost_per_ok": 0.0,
|
|
193
|
+
"cost_per_failed": 0.0,
|
|
194
|
+
"cost_per_empty": 0.0,
|
|
195
|
+
"cost_per_hung": 0.0,
|
|
196
|
+
},
|
|
197
|
+
"model_mix_trend": {},
|
|
175
198
|
}
|
|
176
199
|
|
|
177
200
|
# If ledger file doesn't exist, return empty summary
|
|
@@ -194,6 +217,9 @@ def get_cost_summary():
|
|
|
194
217
|
result["error"] = "ledger format invalid"
|
|
195
218
|
return result
|
|
196
219
|
|
|
220
|
+
# Track ledger entries for per-week-per-model calculation
|
|
221
|
+
ledger_entries = []
|
|
222
|
+
|
|
197
223
|
# Parse each line
|
|
198
224
|
for line in lines:
|
|
199
225
|
line = line.strip()
|
|
@@ -268,6 +294,16 @@ def get_cost_summary():
|
|
|
268
294
|
result["skipped_lines"] += 1
|
|
269
295
|
continue
|
|
270
296
|
|
|
297
|
+
# Store ledger entry for per-week calculation
|
|
298
|
+
ledger_entries.append({
|
|
299
|
+
"timestamp": timestamp,
|
|
300
|
+
"date_str": date_str,
|
|
301
|
+
"model": model,
|
|
302
|
+
"tokens_in": tokens_in,
|
|
303
|
+
"tokens_out": tokens_out,
|
|
304
|
+
"verdict": verdict
|
|
305
|
+
})
|
|
306
|
+
|
|
271
307
|
# Aggregate by model
|
|
272
308
|
if model not in result["models"]:
|
|
273
309
|
result["models"][model] = {
|
|
@@ -329,9 +365,192 @@ def get_cost_summary():
|
|
|
329
365
|
"total_cost": total_cost,
|
|
330
366
|
}
|
|
331
367
|
|
|
368
|
+
# Calculate per-week costs and model mix trend
|
|
369
|
+
_calculate_weekly_costs(result, pricing_map, ledger_entries)
|
|
370
|
+
_calculate_verdict_weighted_cost(result, pricing_map)
|
|
371
|
+
_calculate_model_mix_trend(result)
|
|
372
|
+
|
|
332
373
|
return result
|
|
333
374
|
|
|
334
375
|
|
|
376
|
+
def _calculate_weekly_costs(result, pricing_map, ledger_entries):
|
|
377
|
+
"""Calculate per-week cost rollup from ledger entries.
|
|
378
|
+
|
|
379
|
+
Groups ledger entries by ISO week (YYYY-Www format) and aggregates
|
|
380
|
+
per-model tokens for each week. If pricing is available, includes cost estimates.
|
|
381
|
+
|
|
382
|
+
BUG FIX: This function now uses EACH WEEK'S OWN per-model token counts
|
|
383
|
+
from the ledger, not the global model distribution. This prevents inflating
|
|
384
|
+
each week's cost by applying global model mix that may not be accurate
|
|
385
|
+
for that particular week.
|
|
386
|
+
|
|
387
|
+
PERF FIX: datetime.strptime is hoisted outside the inner loops. Each timestamp
|
|
388
|
+
is parsed ONCE during the initial pass, avoiding O(weeks*models*entries) re-parsing.
|
|
389
|
+
|
|
390
|
+
Modifies result["per_week_costs"] in-place with structure:
|
|
391
|
+
{
|
|
392
|
+
"YYYY-Www": {
|
|
393
|
+
"tokens_in": int,
|
|
394
|
+
"tokens_out": int,
|
|
395
|
+
"model_tokens": {"model": int, ...},
|
|
396
|
+
"cost": float (if pricing available)
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
"""
|
|
400
|
+
from datetime import datetime
|
|
401
|
+
|
|
402
|
+
if not ledger_entries:
|
|
403
|
+
return
|
|
404
|
+
|
|
405
|
+
# OPTIMIZATION: Pre-parse all timestamps once (avoid O(weeks*models*entries) re-parsing)
|
|
406
|
+
# Map each entry index to its ISO week key, computed upfront
|
|
407
|
+
entry_weeks = {}
|
|
408
|
+
for idx, entry in enumerate(ledger_entries):
|
|
409
|
+
try:
|
|
410
|
+
# Parse YYYY-MM-DD to ISO week
|
|
411
|
+
dt = datetime.strptime(entry["date_str"], "%Y-%m-%d")
|
|
412
|
+
iso_year, iso_week, _ = dt.isocalendar()
|
|
413
|
+
week_key = f"{iso_year}-W{iso_week:02d}"
|
|
414
|
+
entry_weeks[idx] = week_key
|
|
415
|
+
except (ValueError, KeyError):
|
|
416
|
+
entry_weeks[idx] = None
|
|
417
|
+
|
|
418
|
+
# Group ledger entries by ISO week and aggregate per-model within each week
|
|
419
|
+
weeks = {}
|
|
420
|
+
for idx, entry in enumerate(ledger_entries):
|
|
421
|
+
week_key = entry_weeks[idx]
|
|
422
|
+
if week_key is None:
|
|
423
|
+
# Skip entries with invalid dates or missing fields
|
|
424
|
+
continue
|
|
425
|
+
|
|
426
|
+
if week_key not in weeks:
|
|
427
|
+
weeks[week_key] = {
|
|
428
|
+
"tokens_in": 0,
|
|
429
|
+
"tokens_out": 0,
|
|
430
|
+
"model_tokens": {},
|
|
431
|
+
"cost": 0.0
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
# Aggregate this entry's tokens into the week
|
|
435
|
+
weeks[week_key]["tokens_in"] += entry["tokens_in"]
|
|
436
|
+
weeks[week_key]["tokens_out"] += entry["tokens_out"]
|
|
437
|
+
|
|
438
|
+
# Track per-model tokens within this week
|
|
439
|
+
model = entry["model"]
|
|
440
|
+
if model not in weeks[week_key]["model_tokens"]:
|
|
441
|
+
weeks[week_key]["model_tokens"][model] = 0
|
|
442
|
+
weeks[week_key]["model_tokens"][model] += entry["tokens_in"] + entry["tokens_out"]
|
|
443
|
+
|
|
444
|
+
# If pricing available, calculate cost per week based on THAT WEEK'S model mix
|
|
445
|
+
if pricing_map:
|
|
446
|
+
for week_key, week_data in weeks.items():
|
|
447
|
+
total_cost = 0.0
|
|
448
|
+
for model, total_tokens in week_data["model_tokens"].items():
|
|
449
|
+
if model in pricing_map:
|
|
450
|
+
pricing = pricing_map[model]
|
|
451
|
+
input_price = pricing.get("input_per_mtok", 0.0)
|
|
452
|
+
output_price = pricing.get("output_per_mtok", 0.0)
|
|
453
|
+
# Use the pre-parsed week info to find entries for this model in this week
|
|
454
|
+
model_entries_in_week = [
|
|
455
|
+
ledger_entries[idx]
|
|
456
|
+
for idx, w_key in entry_weeks.items()
|
|
457
|
+
if w_key == week_key and ledger_entries[idx]["model"] == model
|
|
458
|
+
]
|
|
459
|
+
if model_entries_in_week:
|
|
460
|
+
week_model_tokens_in = sum(e["tokens_in"] for e in model_entries_in_week)
|
|
461
|
+
week_model_tokens_out = sum(e["tokens_out"] for e in model_entries_in_week)
|
|
462
|
+
model_cost = (week_model_tokens_in * input_price + week_model_tokens_out * output_price) / 1_000_000
|
|
463
|
+
else:
|
|
464
|
+
# Fallback: if we can't find the entries, use 1:2 ratio estimate
|
|
465
|
+
tokens_in_estimate = total_tokens * 0.333
|
|
466
|
+
tokens_out_estimate = total_tokens * 0.667
|
|
467
|
+
model_cost = (tokens_in_estimate * input_price + tokens_out_estimate * output_price) / 1_000_000
|
|
468
|
+
total_cost += model_cost
|
|
469
|
+
week_data["cost"] = total_cost
|
|
470
|
+
|
|
471
|
+
result["per_week_costs"] = weeks
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def _calculate_verdict_weighted_cost(result, pricing_map):
|
|
475
|
+
"""Calculate cost-per-outcome metrics weighted by verdict distribution.
|
|
476
|
+
|
|
477
|
+
Computes cost per successful outcome (cost / ok_count) and cost per other outcomes.
|
|
478
|
+
If pricing is available, uses estimated costs; otherwise uses token counts as proxy.
|
|
479
|
+
|
|
480
|
+
Modifies result["verdict_weighted_cost"] in-place with structure:
|
|
481
|
+
{
|
|
482
|
+
"cost_per_ok": float,
|
|
483
|
+
"cost_per_failed": float,
|
|
484
|
+
"cost_per_empty": float,
|
|
485
|
+
"cost_per_hung": float,
|
|
486
|
+
}
|
|
487
|
+
"""
|
|
488
|
+
scorecard = result["overall_scorecard"]
|
|
489
|
+
|
|
490
|
+
# Calculate total cost (if pricing available)
|
|
491
|
+
total_cost = 0.0
|
|
492
|
+
if pricing_map and result["has_pricing"]:
|
|
493
|
+
for estimate in result["estimates_by_model"].values():
|
|
494
|
+
total_cost += estimate.get("total_cost", 0.0)
|
|
495
|
+
else:
|
|
496
|
+
# Use token count as cost proxy (tokens_in + tokens_out)
|
|
497
|
+
for daily in result["daily_totals"].values():
|
|
498
|
+
total_cost += daily["tokens_in"] + daily["tokens_out"]
|
|
499
|
+
|
|
500
|
+
# Calculate cost per outcome type
|
|
501
|
+
result["verdict_weighted_cost"] = {
|
|
502
|
+
"cost_per_ok": total_cost / scorecard["ok_count"] if scorecard["ok_count"] > 0 else 0.0,
|
|
503
|
+
"cost_per_failed": total_cost / scorecard["failed_count"] if scorecard["failed_count"] > 0 else 0.0,
|
|
504
|
+
"cost_per_empty": total_cost / scorecard["empty_count"] if scorecard["empty_count"] > 0 else 0.0,
|
|
505
|
+
"cost_per_hung": total_cost / scorecard["hung_count"] if scorecard["hung_count"] > 0 else 0.0,
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def _calculate_model_mix_trend(result):
|
|
510
|
+
"""Calculate per-day model usage distribution as percentages.
|
|
511
|
+
|
|
512
|
+
Breaks down the token usage by model for each day in daily_totals.
|
|
513
|
+
Modifies result["model_mix_trend"] in-place with structure:
|
|
514
|
+
{
|
|
515
|
+
"YYYY-MM-DD": {
|
|
516
|
+
"model": percentage (0.0-100.0),
|
|
517
|
+
...
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
"""
|
|
521
|
+
model_mix_trend = {}
|
|
522
|
+
|
|
523
|
+
# For each day, calculate model distribution
|
|
524
|
+
# Since daily_totals doesn't track per-model breakdown, we need to estimate
|
|
525
|
+
# based on the overall model distribution across all runs in that day
|
|
526
|
+
if not result["models"]:
|
|
527
|
+
result["model_mix_trend"] = model_mix_trend
|
|
528
|
+
return
|
|
529
|
+
|
|
530
|
+
# Calculate overall model token distribution
|
|
531
|
+
total_model_tokens = {}
|
|
532
|
+
grand_total_tokens = 0
|
|
533
|
+
for model, stats in result["models"].items():
|
|
534
|
+
tokens = stats["tokens_in"] + stats["tokens_out"]
|
|
535
|
+
total_model_tokens[model] = tokens
|
|
536
|
+
grand_total_tokens += tokens
|
|
537
|
+
|
|
538
|
+
# Apply model distribution to each day (as a simplified proxy)
|
|
539
|
+
for date_str in result["daily_totals"].keys():
|
|
540
|
+
daily_dist = {}
|
|
541
|
+
daily_total = result["daily_totals"][date_str]["tokens_in"] + result["daily_totals"][date_str]["tokens_out"]
|
|
542
|
+
|
|
543
|
+
if grand_total_tokens > 0 and daily_total > 0:
|
|
544
|
+
for model, total_tokens in total_model_tokens.items():
|
|
545
|
+
# Distribute daily tokens proportionally to model usage
|
|
546
|
+
ratio = total_tokens / grand_total_tokens
|
|
547
|
+
daily_dist[model] = round(ratio * 100.0, 2)
|
|
548
|
+
|
|
549
|
+
model_mix_trend[date_str] = daily_dist
|
|
550
|
+
|
|
551
|
+
result["model_mix_trend"] = model_mix_trend
|
|
552
|
+
|
|
553
|
+
|
|
335
554
|
def _load_pricing_config():
|
|
336
555
|
"""Load pricing map from aesop.config.json at call time.
|
|
337
556
|
|
package/ui/handler.py
CHANGED
|
@@ -12,8 +12,15 @@ from pathlib import Path
|
|
|
12
12
|
import config
|
|
13
13
|
import cost
|
|
14
14
|
import csrf
|
|
15
|
+
import quality_scorecard
|
|
15
16
|
import sse
|
|
16
17
|
import wave_prs
|
|
18
|
+
import wave_telemetry
|
|
19
|
+
import wave_failure
|
|
20
|
+
import wave_dispatch
|
|
21
|
+
import wave_gantt
|
|
22
|
+
import wave_audit_tail
|
|
23
|
+
import wave_reasoning_tail
|
|
17
24
|
import api
|
|
18
25
|
import api.tracker
|
|
19
26
|
import api.submit
|
|
@@ -197,6 +204,20 @@ class DashboardHandler(http.server.BaseHTTPRequestHandler):
|
|
|
197
204
|
self.serve_api_cost()
|
|
198
205
|
elif self.path == "/api/wave/prs":
|
|
199
206
|
self.serve_api_wave_prs()
|
|
207
|
+
elif self.path == "/api/wave/telemetry":
|
|
208
|
+
self.serve_api_wave_telemetry()
|
|
209
|
+
elif self.path == "/api/wave/dispatch":
|
|
210
|
+
self.serve_api_wave_dispatch()
|
|
211
|
+
elif self.path == "/api/wave/gantt":
|
|
212
|
+
self.serve_api_wave_gantt()
|
|
213
|
+
elif self.path == "/api/wave/audit-tail":
|
|
214
|
+
self.serve_api_wave_audit_tail()
|
|
215
|
+
elif self.path == "/api/wave/reasoning-tail":
|
|
216
|
+
self.serve_api_wave_reasoning_tail()
|
|
217
|
+
elif self.path == "/api/wave/quality-scorecards":
|
|
218
|
+
self.serve_api_wave_quality_scorecards()
|
|
219
|
+
elif self.path.startswith("/api/wave/failure"):
|
|
220
|
+
self.serve_api_wave_failure()
|
|
200
221
|
elif self.path == "/api/backlog":
|
|
201
222
|
self.serve_backlog()
|
|
202
223
|
elif self.path == "/api/agents":
|
|
@@ -427,6 +448,168 @@ class DashboardHandler(http.server.BaseHTTPRequestHandler):
|
|
|
427
448
|
self.end_headers()
|
|
428
449
|
self.wfile.write(json.dumps({"error": "Internal server error"}).encode('utf-8'))
|
|
429
450
|
|
|
451
|
+
def serve_api_wave_telemetry(self):
|
|
452
|
+
"""GET /api/wave/telemetry — current wave phase, cost metrics, and top blocker.
|
|
453
|
+
|
|
454
|
+
Read-only; reads state at call time (no caching). Returns wave phase info,
|
|
455
|
+
current cost metrics, and top blocker from AUDIT-BACKLOG.md.
|
|
456
|
+
"""
|
|
457
|
+
try:
|
|
458
|
+
payload = wave_telemetry.get_wave_telemetry()
|
|
459
|
+
self.send_response(200)
|
|
460
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
461
|
+
self.send_header("Cache-Control", "no-cache, no-store, must-revalidate")
|
|
462
|
+
self.end_headers()
|
|
463
|
+
self.wfile.write(json.dumps(payload, default=str).encode('utf-8'))
|
|
464
|
+
except Exception as e:
|
|
465
|
+
print(f"[serve_api_wave_telemetry] Uncaught exception: {e}", file=sys.stderr)
|
|
466
|
+
self.send_response(500)
|
|
467
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
468
|
+
self.end_headers()
|
|
469
|
+
self.wfile.write(json.dumps({"error": "Internal server error"}).encode('utf-8'))
|
|
470
|
+
|
|
471
|
+
def serve_api_wave_dispatch(self):
|
|
472
|
+
"""GET /api/wave/dispatch — live per-agent phase and activity visibility.
|
|
473
|
+
|
|
474
|
+
Read-only; reads at call time (no caching). Returns per-agent phase,
|
|
475
|
+
last-activity age, and token burn estimates. Degrades to {available:false}
|
|
476
|
+
when no active workflow.
|
|
477
|
+
"""
|
|
478
|
+
try:
|
|
479
|
+
payload = wave_dispatch.get_wave_dispatch()
|
|
480
|
+
self.send_response(200)
|
|
481
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
482
|
+
self.send_header("Cache-Control", "no-cache, no-store, must-revalidate")
|
|
483
|
+
self.end_headers()
|
|
484
|
+
self.wfile.write(json.dumps(payload, default=str).encode('utf-8'))
|
|
485
|
+
except Exception as e:
|
|
486
|
+
print(f"[serve_api_wave_dispatch] Uncaught exception: {e}", file=sys.stderr)
|
|
487
|
+
self.send_response(500)
|
|
488
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
489
|
+
self.end_headers()
|
|
490
|
+
self.wfile.write(json.dumps({"error": "Internal server error"}).encode('utf-8'))
|
|
491
|
+
|
|
492
|
+
def serve_api_wave_gantt(self):
|
|
493
|
+
"""GET /api/wave/gantt — Gantt timeline data for agents in current wave.
|
|
494
|
+
|
|
495
|
+
Read-only; reads at call time (no caching). Returns per-agent rows with
|
|
496
|
+
phase timing spans suitable for Gantt visualization. Degrades to
|
|
497
|
+
{available:false} when no active workflow.
|
|
498
|
+
"""
|
|
499
|
+
try:
|
|
500
|
+
payload = wave_gantt.get_wave_gantt()
|
|
501
|
+
self.send_response(200)
|
|
502
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
503
|
+
self.send_header("Cache-Control", "no-cache, no-store, must-revalidate")
|
|
504
|
+
self.end_headers()
|
|
505
|
+
self.wfile.write(json.dumps(payload, default=str).encode('utf-8'))
|
|
506
|
+
except Exception as e:
|
|
507
|
+
print(f"[serve_api_wave_gantt] Uncaught exception: {e}", file=sys.stderr)
|
|
508
|
+
self.send_response(500)
|
|
509
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
510
|
+
self.end_headers()
|
|
511
|
+
self.wfile.write(json.dumps({"error": "Internal server error"}).encode('utf-8'))
|
|
512
|
+
|
|
513
|
+
def serve_api_wave_audit_tail(self):
|
|
514
|
+
"""GET /api/wave/audit-tail — latest audit/verification outcomes.
|
|
515
|
+
|
|
516
|
+
Read-only; reads at call time (no caching). Returns recent audit backlog
|
|
517
|
+
items and ledger verdicts as a compact tail. Shows latest findings from
|
|
518
|
+
adversarial reviews and verify verdicts.
|
|
519
|
+
"""
|
|
520
|
+
try:
|
|
521
|
+
payload = wave_audit_tail.get_wave_audit_tail()
|
|
522
|
+
self.send_response(200)
|
|
523
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
524
|
+
self.send_header("Cache-Control", "no-cache, no-store, must-revalidate")
|
|
525
|
+
self.end_headers()
|
|
526
|
+
self.wfile.write(json.dumps(payload, default=str).encode('utf-8'))
|
|
527
|
+
except Exception as e:
|
|
528
|
+
print(f"[serve_api_wave_audit_tail] Uncaught exception: {e}", file=sys.stderr)
|
|
529
|
+
self.send_response(500)
|
|
530
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
531
|
+
self.end_headers()
|
|
532
|
+
self.wfile.write(json.dumps({"error": "Internal server error"}).encode('utf-8'))
|
|
533
|
+
|
|
534
|
+
def serve_api_wave_reasoning_tail(self):
|
|
535
|
+
"""GET /api/wave/reasoning-tail — per-agent live reasoning/transcript summary.
|
|
536
|
+
|
|
537
|
+
Read-only; reads at call time (no caching). Returns latest transcript activity
|
|
538
|
+
summary for each live agent (redacted). Shows thinking/tool-call sequences
|
|
539
|
+
in a compact format suitable for Activity view transparency.
|
|
540
|
+
"""
|
|
541
|
+
try:
|
|
542
|
+
payload = wave_reasoning_tail.get_wave_reasoning_tail()
|
|
543
|
+
self.send_response(200)
|
|
544
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
545
|
+
self.send_header("Cache-Control", "no-cache, no-store, must-revalidate")
|
|
546
|
+
self.end_headers()
|
|
547
|
+
self.wfile.write(json.dumps(payload, default=str).encode('utf-8'))
|
|
548
|
+
except Exception as e:
|
|
549
|
+
print(f"[serve_api_wave_reasoning_tail] Uncaught exception: {e}", file=sys.stderr)
|
|
550
|
+
self.send_response(500)
|
|
551
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
552
|
+
self.end_headers()
|
|
553
|
+
self.wfile.write(json.dumps({"error": "Internal server error"}).encode('utf-8'))
|
|
554
|
+
|
|
555
|
+
def serve_api_wave_quality_scorecards(self):
|
|
556
|
+
"""GET /api/wave/quality-scorecards — per-agent-specialty quality metrics.
|
|
557
|
+
|
|
558
|
+
Read-only; reads at call time (no caching). Returns per-specialty quality:
|
|
559
|
+
success rate (green/total) and retry/repair frequency from ledger.
|
|
560
|
+
Includes top rankings by success and retry frequency.
|
|
561
|
+
"""
|
|
562
|
+
try:
|
|
563
|
+
payload = quality_scorecard.get_quality_scorecard()
|
|
564
|
+
self.send_response(200)
|
|
565
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
566
|
+
self.send_header("Cache-Control", "no-cache, no-store, must-revalidate")
|
|
567
|
+
self.end_headers()
|
|
568
|
+
self.wfile.write(json.dumps(payload, default=str).encode('utf-8'))
|
|
569
|
+
except Exception as e:
|
|
570
|
+
print(f"[serve_api_wave_quality_scorecards] Uncaught exception: {e}", file=sys.stderr)
|
|
571
|
+
self.send_response(500)
|
|
572
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
573
|
+
self.end_headers()
|
|
574
|
+
self.wfile.write(json.dumps({"error": "Internal server error"}).encode('utf-8'))
|
|
575
|
+
|
|
576
|
+
def serve_api_wave_failure(self):
|
|
577
|
+
"""GET /api/wave/failure?pr=N — CI job logs and failure details for a PR.
|
|
578
|
+
|
|
579
|
+
Read-only; shells `gh run view --json jobs` / `gh api .../logs` (short timeout,
|
|
580
|
+
cached a few seconds). Degrades to {available:false, error:...} when gh is
|
|
581
|
+
missing or un-authenticated — never a 500 for those.
|
|
582
|
+
|
|
583
|
+
Query params:
|
|
584
|
+
pr: PR number (required)
|
|
585
|
+
"""
|
|
586
|
+
try:
|
|
587
|
+
# Parse query string for pr parameter
|
|
588
|
+
query = urllib.parse.urlparse(self.path).query
|
|
589
|
+
params = urllib.parse.parse_qs(query)
|
|
590
|
+
pr_str = params.get('pr', [None])[0]
|
|
591
|
+
|
|
592
|
+
if not pr_str or not pr_str.isdigit():
|
|
593
|
+
self.send_response(400)
|
|
594
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
595
|
+
self.end_headers()
|
|
596
|
+
self.wfile.write(json.dumps({"error": "pr parameter required and must be a number"}).encode('utf-8'))
|
|
597
|
+
return
|
|
598
|
+
|
|
599
|
+
pr_number = int(pr_str)
|
|
600
|
+
payload = wave_failure.get_wave_failure(pr_number)
|
|
601
|
+
self.send_response(200)
|
|
602
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
603
|
+
self.send_header("Cache-Control", "no-cache, no-store, must-revalidate")
|
|
604
|
+
self.end_headers()
|
|
605
|
+
self.wfile.write(json.dumps(payload, default=str).encode('utf-8'))
|
|
606
|
+
except Exception as e:
|
|
607
|
+
print(f"[serve_api_wave_failure] Uncaught exception: {e}", file=sys.stderr)
|
|
608
|
+
self.send_response(500)
|
|
609
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
610
|
+
self.end_headers()
|
|
611
|
+
self.wfile.write(json.dumps({"error": "Internal server error"}).encode('utf-8'))
|
|
612
|
+
|
|
430
613
|
def serve_data(self):
|
|
431
614
|
"""Serve dashboard data as JSON."""
|
|
432
615
|
data = {
|