@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
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
|
|
@@ -75,6 +82,32 @@ _ASSET_MIME = {
|
|
|
75
82
|
}
|
|
76
83
|
|
|
77
84
|
|
|
85
|
+
def _is_client_disconnect_error(exc):
|
|
86
|
+
"""True if exception indicates a normal client disconnect (not an error).
|
|
87
|
+
|
|
88
|
+
When clients close connections mid-stream (tab closed, network drop, etc.),
|
|
89
|
+
we get BrokenPipeError, ConnectionAbortedError, or ConnectionResetError.
|
|
90
|
+
On Windows, rare plain OSError with winerror 10053/10054 may also indicate
|
|
91
|
+
a disconnect. These are normal lifecycle events, not bugs, and should not
|
|
92
|
+
be logged as errors.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
exc: The exception to check
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
bool: True if this is a normal disconnect, False if it's an unexpected error
|
|
99
|
+
"""
|
|
100
|
+
if isinstance(exc, (BrokenPipeError, ConnectionAbortedError,
|
|
101
|
+
ConnectionResetError)):
|
|
102
|
+
return True
|
|
103
|
+
if isinstance(exc, OSError):
|
|
104
|
+
# Windows disconnect errors: 10053 (WSAECONNABORTED), 10054 (WSAECONNRESET)
|
|
105
|
+
winerror = getattr(exc, 'winerror', None)
|
|
106
|
+
if winerror in (10053, 10054):
|
|
107
|
+
return True
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
|
|
78
111
|
def _is_local_origin(origin):
|
|
79
112
|
"""True if an Origin header value is on the local allowlist.
|
|
80
113
|
|
|
@@ -197,6 +230,20 @@ class DashboardHandler(http.server.BaseHTTPRequestHandler):
|
|
|
197
230
|
self.serve_api_cost()
|
|
198
231
|
elif self.path == "/api/wave/prs":
|
|
199
232
|
self.serve_api_wave_prs()
|
|
233
|
+
elif self.path == "/api/wave/telemetry":
|
|
234
|
+
self.serve_api_wave_telemetry()
|
|
235
|
+
elif self.path == "/api/wave/dispatch":
|
|
236
|
+
self.serve_api_wave_dispatch()
|
|
237
|
+
elif self.path == "/api/wave/gantt":
|
|
238
|
+
self.serve_api_wave_gantt()
|
|
239
|
+
elif self.path == "/api/wave/audit-tail":
|
|
240
|
+
self.serve_api_wave_audit_tail()
|
|
241
|
+
elif self.path == "/api/wave/reasoning-tail":
|
|
242
|
+
self.serve_api_wave_reasoning_tail()
|
|
243
|
+
elif self.path == "/api/wave/quality-scorecards":
|
|
244
|
+
self.serve_api_wave_quality_scorecards()
|
|
245
|
+
elif self.path.startswith("/api/wave/failure"):
|
|
246
|
+
self.serve_api_wave_failure()
|
|
200
247
|
elif self.path == "/api/backlog":
|
|
201
248
|
self.serve_backlog()
|
|
202
249
|
elif self.path == "/api/agents":
|
|
@@ -314,10 +361,16 @@ class DashboardHandler(http.server.BaseHTTPRequestHandler):
|
|
|
314
361
|
self.send_header("Cache-Control", "public, max-age=31536000, immutable")
|
|
315
362
|
self.end_headers()
|
|
316
363
|
self.wfile.write(content)
|
|
317
|
-
except (BrokenPipeError, ConnectionAbortedError, ConnectionResetError
|
|
364
|
+
except (BrokenPipeError, ConnectionAbortedError, ConnectionResetError):
|
|
318
365
|
pass # client went away mid-response — normal, not an error
|
|
366
|
+
except OSError as e:
|
|
367
|
+
if not _is_client_disconnect_error(e):
|
|
368
|
+
print(f"[serve_asset] Uncaught OSError: {e}", file=sys.stderr)
|
|
369
|
+
self.send_error(500)
|
|
370
|
+
# else: silently pass for disconnect errors
|
|
319
371
|
except Exception as e:
|
|
320
|
-
|
|
372
|
+
if not _is_client_disconnect_error(e):
|
|
373
|
+
print(f"[serve_asset] Uncaught exception: {e}", file=sys.stderr)
|
|
321
374
|
self.send_error(500)
|
|
322
375
|
|
|
323
376
|
def serve_api_state(self):
|
|
@@ -355,11 +408,16 @@ class DashboardHandler(http.server.BaseHTTPRequestHandler):
|
|
|
355
408
|
self.end_headers()
|
|
356
409
|
self.wfile.write(json.dumps(state, default=str).encode('utf-8'))
|
|
357
410
|
except Exception as e:
|
|
411
|
+
if _is_client_disconnect_error(e):
|
|
412
|
+
return # client gone: nothing to send or log
|
|
358
413
|
print(f"[serve_api_state] Uncaught exception: {e}", file=sys.stderr)
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
414
|
+
try:
|
|
415
|
+
self.send_response(500)
|
|
416
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
417
|
+
self.end_headers()
|
|
418
|
+
self.wfile.write(json.dumps({"error": "Internal server error"}).encode('utf-8'))
|
|
419
|
+
except Exception:
|
|
420
|
+
pass # best-effort error response; client may be gone
|
|
363
421
|
|
|
364
422
|
def serve_api_session(self):
|
|
365
423
|
"""GET /api/session — the CSRF token for same-origin JS (plan D3.3).
|
|
@@ -400,11 +458,16 @@ class DashboardHandler(http.server.BaseHTTPRequestHandler):
|
|
|
400
458
|
self.end_headers()
|
|
401
459
|
self.wfile.write(json.dumps(summary, default=str).encode('utf-8'))
|
|
402
460
|
except Exception as e:
|
|
461
|
+
if _is_client_disconnect_error(e):
|
|
462
|
+
return # client gone: nothing to send or log
|
|
403
463
|
print(f"[serve_api_cost] Uncaught exception: {e}", file=sys.stderr)
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
464
|
+
try:
|
|
465
|
+
self.send_response(500)
|
|
466
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
467
|
+
self.end_headers()
|
|
468
|
+
self.wfile.write(json.dumps({"error": "Internal server error"}).encode('utf-8'))
|
|
469
|
+
except Exception:
|
|
470
|
+
pass # best-effort error response; client may be gone
|
|
408
471
|
|
|
409
472
|
def serve_api_wave_prs(self):
|
|
410
473
|
"""GET /api/wave/prs — open PRs + PR-less feat/* branches for the PR board.
|
|
@@ -421,28 +484,244 @@ class DashboardHandler(http.server.BaseHTTPRequestHandler):
|
|
|
421
484
|
self.end_headers()
|
|
422
485
|
self.wfile.write(json.dumps(payload, default=str).encode('utf-8'))
|
|
423
486
|
except Exception as e:
|
|
487
|
+
if _is_client_disconnect_error(e):
|
|
488
|
+
return # client gone: nothing to send or log
|
|
424
489
|
print(f"[serve_api_wave_prs] Uncaught exception: {e}", file=sys.stderr)
|
|
425
|
-
|
|
490
|
+
try:
|
|
491
|
+
self.send_response(500)
|
|
492
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
493
|
+
self.end_headers()
|
|
494
|
+
self.wfile.write(json.dumps({"error": "Internal server error"}).encode('utf-8'))
|
|
495
|
+
except Exception:
|
|
496
|
+
pass # best-effort error response; client may be gone
|
|
497
|
+
|
|
498
|
+
def serve_api_wave_telemetry(self):
|
|
499
|
+
"""GET /api/wave/telemetry — current wave phase, cost metrics, and top blocker.
|
|
500
|
+
|
|
501
|
+
Read-only; reads state at call time (no caching). Returns wave phase info,
|
|
502
|
+
current cost metrics, and top blocker from AUDIT-BACKLOG.md.
|
|
503
|
+
"""
|
|
504
|
+
try:
|
|
505
|
+
payload = wave_telemetry.get_wave_telemetry()
|
|
506
|
+
self.send_response(200)
|
|
426
507
|
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
508
|
+
self.send_header("Cache-Control", "no-cache, no-store, must-revalidate")
|
|
427
509
|
self.end_headers()
|
|
428
|
-
self.wfile.write(json.dumps(
|
|
510
|
+
self.wfile.write(json.dumps(payload, default=str).encode('utf-8'))
|
|
511
|
+
except Exception as e:
|
|
512
|
+
if _is_client_disconnect_error(e):
|
|
513
|
+
return # client gone: nothing to send or log
|
|
514
|
+
print(f"[serve_api_wave_telemetry] Uncaught exception: {e}", file=sys.stderr)
|
|
515
|
+
try:
|
|
516
|
+
self.send_response(500)
|
|
517
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
518
|
+
self.end_headers()
|
|
519
|
+
self.wfile.write(json.dumps({"error": "Internal server error"}).encode('utf-8'))
|
|
520
|
+
except Exception:
|
|
521
|
+
pass # best-effort error response; client may be gone
|
|
522
|
+
|
|
523
|
+
def serve_api_wave_dispatch(self):
|
|
524
|
+
"""GET /api/wave/dispatch — live per-agent phase and activity visibility.
|
|
525
|
+
|
|
526
|
+
Read-only; reads at call time (no caching). Returns per-agent phase,
|
|
527
|
+
last-activity age, and token burn estimates. Degrades to {available:false}
|
|
528
|
+
when no active workflow.
|
|
529
|
+
"""
|
|
530
|
+
try:
|
|
531
|
+
payload = wave_dispatch.get_wave_dispatch()
|
|
532
|
+
self.send_response(200)
|
|
533
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
534
|
+
self.send_header("Cache-Control", "no-cache, no-store, must-revalidate")
|
|
535
|
+
self.end_headers()
|
|
536
|
+
self.wfile.write(json.dumps(payload, default=str).encode('utf-8'))
|
|
537
|
+
except Exception as e:
|
|
538
|
+
if _is_client_disconnect_error(e):
|
|
539
|
+
return # client gone: nothing to send or log
|
|
540
|
+
print(f"[serve_api_wave_dispatch] Uncaught exception: {e}", file=sys.stderr)
|
|
541
|
+
try:
|
|
542
|
+
self.send_response(500)
|
|
543
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
544
|
+
self.end_headers()
|
|
545
|
+
self.wfile.write(json.dumps({"error": "Internal server error"}).encode('utf-8'))
|
|
546
|
+
except Exception:
|
|
547
|
+
pass # best-effort error response; client may be gone
|
|
548
|
+
|
|
549
|
+
def serve_api_wave_gantt(self):
|
|
550
|
+
"""GET /api/wave/gantt — Gantt timeline data for agents in current wave.
|
|
551
|
+
|
|
552
|
+
Read-only; reads at call time (no caching). Returns per-agent rows with
|
|
553
|
+
phase timing spans suitable for Gantt visualization. Degrades to
|
|
554
|
+
{available:false} when no active workflow.
|
|
555
|
+
"""
|
|
556
|
+
try:
|
|
557
|
+
payload = wave_gantt.get_wave_gantt()
|
|
558
|
+
self.send_response(200)
|
|
559
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
560
|
+
self.send_header("Cache-Control", "no-cache, no-store, must-revalidate")
|
|
561
|
+
self.end_headers()
|
|
562
|
+
self.wfile.write(json.dumps(payload, default=str).encode('utf-8'))
|
|
563
|
+
except Exception as e:
|
|
564
|
+
if _is_client_disconnect_error(e):
|
|
565
|
+
return # client gone: nothing to send or log
|
|
566
|
+
print(f"[serve_api_wave_gantt] Uncaught exception: {e}", file=sys.stderr)
|
|
567
|
+
try:
|
|
568
|
+
self.send_response(500)
|
|
569
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
570
|
+
self.end_headers()
|
|
571
|
+
self.wfile.write(json.dumps({"error": "Internal server error"}).encode('utf-8'))
|
|
572
|
+
except Exception:
|
|
573
|
+
pass # best-effort error response; client may be gone
|
|
574
|
+
|
|
575
|
+
def serve_api_wave_audit_tail(self):
|
|
576
|
+
"""GET /api/wave/audit-tail — latest audit/verification outcomes.
|
|
577
|
+
|
|
578
|
+
Read-only; reads at call time (no caching). Returns recent audit backlog
|
|
579
|
+
items and ledger verdicts as a compact tail. Shows latest findings from
|
|
580
|
+
adversarial reviews and verify verdicts.
|
|
581
|
+
"""
|
|
582
|
+
try:
|
|
583
|
+
payload = wave_audit_tail.get_wave_audit_tail()
|
|
584
|
+
self.send_response(200)
|
|
585
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
586
|
+
self.send_header("Cache-Control", "no-cache, no-store, must-revalidate")
|
|
587
|
+
self.end_headers()
|
|
588
|
+
self.wfile.write(json.dumps(payload, default=str).encode('utf-8'))
|
|
589
|
+
except Exception as e:
|
|
590
|
+
if _is_client_disconnect_error(e):
|
|
591
|
+
return # client gone: nothing to send or log
|
|
592
|
+
print(f"[serve_api_wave_audit_tail] Uncaught exception: {e}", file=sys.stderr)
|
|
593
|
+
try:
|
|
594
|
+
self.send_response(500)
|
|
595
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
596
|
+
self.end_headers()
|
|
597
|
+
self.wfile.write(json.dumps({"error": "Internal server error"}).encode('utf-8'))
|
|
598
|
+
except Exception:
|
|
599
|
+
pass # best-effort error response; client may be gone
|
|
600
|
+
|
|
601
|
+
def serve_api_wave_reasoning_tail(self):
|
|
602
|
+
"""GET /api/wave/reasoning-tail — per-agent live reasoning/transcript summary.
|
|
603
|
+
|
|
604
|
+
Read-only; reads at call time (no caching). Returns latest transcript activity
|
|
605
|
+
summary for each live agent (redacted). Shows thinking/tool-call sequences
|
|
606
|
+
in a compact format suitable for Activity view transparency.
|
|
607
|
+
"""
|
|
608
|
+
try:
|
|
609
|
+
payload = wave_reasoning_tail.get_wave_reasoning_tail()
|
|
610
|
+
self.send_response(200)
|
|
611
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
612
|
+
self.send_header("Cache-Control", "no-cache, no-store, must-revalidate")
|
|
613
|
+
self.end_headers()
|
|
614
|
+
self.wfile.write(json.dumps(payload, default=str).encode('utf-8'))
|
|
615
|
+
except Exception as e:
|
|
616
|
+
if _is_client_disconnect_error(e):
|
|
617
|
+
return # client gone: nothing to send or log
|
|
618
|
+
print(f"[serve_api_wave_reasoning_tail] Uncaught exception: {e}", file=sys.stderr)
|
|
619
|
+
try:
|
|
620
|
+
self.send_response(500)
|
|
621
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
622
|
+
self.end_headers()
|
|
623
|
+
self.wfile.write(json.dumps({"error": "Internal server error"}).encode('utf-8'))
|
|
624
|
+
except Exception:
|
|
625
|
+
pass # best-effort error response; client may be gone
|
|
626
|
+
|
|
627
|
+
def serve_api_wave_quality_scorecards(self):
|
|
628
|
+
"""GET /api/wave/quality-scorecards — per-agent-specialty quality metrics.
|
|
629
|
+
|
|
630
|
+
Read-only; reads at call time (no caching). Returns per-specialty quality:
|
|
631
|
+
success rate (green/total) and retry/repair frequency from ledger.
|
|
632
|
+
Includes top rankings by success and retry frequency.
|
|
633
|
+
"""
|
|
634
|
+
try:
|
|
635
|
+
payload = quality_scorecard.get_quality_scorecard()
|
|
636
|
+
self.send_response(200)
|
|
637
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
638
|
+
self.send_header("Cache-Control", "no-cache, no-store, must-revalidate")
|
|
639
|
+
self.end_headers()
|
|
640
|
+
self.wfile.write(json.dumps(payload, default=str).encode('utf-8'))
|
|
641
|
+
except Exception as e:
|
|
642
|
+
if _is_client_disconnect_error(e):
|
|
643
|
+
return # client gone: nothing to send or log
|
|
644
|
+
print(f"[serve_api_wave_quality_scorecards] Uncaught exception: {e}", file=sys.stderr)
|
|
645
|
+
try:
|
|
646
|
+
self.send_response(500)
|
|
647
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
648
|
+
self.end_headers()
|
|
649
|
+
self.wfile.write(json.dumps({"error": "Internal server error"}).encode('utf-8'))
|
|
650
|
+
except Exception:
|
|
651
|
+
pass # best-effort error response; client may be gone
|
|
652
|
+
|
|
653
|
+
def serve_api_wave_failure(self):
|
|
654
|
+
"""GET /api/wave/failure?pr=N — CI job logs and failure details for a PR.
|
|
655
|
+
|
|
656
|
+
Read-only; shells `gh run view --json jobs` / `gh api .../logs` (short timeout,
|
|
657
|
+
cached a few seconds). Degrades to {available:false, error:...} when gh is
|
|
658
|
+
missing or un-authenticated — never a 500 for those.
|
|
659
|
+
|
|
660
|
+
Query params:
|
|
661
|
+
pr: PR number (required)
|
|
662
|
+
"""
|
|
663
|
+
try:
|
|
664
|
+
# Parse query string for pr parameter
|
|
665
|
+
query = urllib.parse.urlparse(self.path).query
|
|
666
|
+
params = urllib.parse.parse_qs(query)
|
|
667
|
+
pr_str = params.get('pr', [None])[0]
|
|
668
|
+
|
|
669
|
+
if not pr_str or not pr_str.isdigit():
|
|
670
|
+
self.send_response(400)
|
|
671
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
672
|
+
self.end_headers()
|
|
673
|
+
self.wfile.write(json.dumps({"error": "pr parameter required and must be a number"}).encode('utf-8'))
|
|
674
|
+
return
|
|
675
|
+
|
|
676
|
+
pr_number = int(pr_str)
|
|
677
|
+
payload = wave_failure.get_wave_failure(pr_number)
|
|
678
|
+
self.send_response(200)
|
|
679
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
680
|
+
self.send_header("Cache-Control", "no-cache, no-store, must-revalidate")
|
|
681
|
+
self.end_headers()
|
|
682
|
+
self.wfile.write(json.dumps(payload, default=str).encode('utf-8'))
|
|
683
|
+
except Exception as e:
|
|
684
|
+
if _is_client_disconnect_error(e):
|
|
685
|
+
return # client gone: nothing to send or log
|
|
686
|
+
print(f"[serve_api_wave_failure] Uncaught exception: {e}", file=sys.stderr)
|
|
687
|
+
try:
|
|
688
|
+
self.send_response(500)
|
|
689
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
690
|
+
self.end_headers()
|
|
691
|
+
self.wfile.write(json.dumps({"error": "Internal server error"}).encode('utf-8'))
|
|
692
|
+
except Exception:
|
|
693
|
+
pass # best-effort error response; client may be gone
|
|
429
694
|
|
|
430
695
|
def serve_data(self):
|
|
431
696
|
"""Serve dashboard data as JSON."""
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
697
|
+
try:
|
|
698
|
+
data = {
|
|
699
|
+
"watchdog": get_heartbeat_status(),
|
|
700
|
+
"monitor": get_monitor_heartbeat_status(),
|
|
701
|
+
"agents": get_fleet_agents(),
|
|
702
|
+
"repos": get_repos_status(),
|
|
703
|
+
"events": get_recent_events(),
|
|
704
|
+
"alerts": get_alerts(),
|
|
705
|
+
"messages": get_main_thread_messages(),
|
|
706
|
+
}
|
|
707
|
+
self.send_response(200)
|
|
708
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
709
|
+
self.send_header("Cache-Control", "no-cache, no-store, must-revalidate")
|
|
710
|
+
self.end_headers()
|
|
711
|
+
self.wfile.write(json.dumps(data, default=str).encode('utf-8'))
|
|
712
|
+
except Exception as e:
|
|
713
|
+
if _is_client_disconnect_error(e):
|
|
714
|
+
return # client gone: nothing to send or log
|
|
715
|
+
print(f"[serve_data] Uncaught exception: {e}", file=sys.stderr)
|
|
716
|
+
try:
|
|
717
|
+
# Best-effort 500 response
|
|
718
|
+
self.send_response(500)
|
|
719
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
720
|
+
self.end_headers()
|
|
721
|
+
self.wfile.write(json.dumps({"error": "Internal server error"}).encode('utf-8'))
|
|
722
|
+
except Exception:
|
|
723
|
+
# Sending to a dead client must not cascade
|
|
724
|
+
pass
|
|
446
725
|
|
|
447
726
|
def serve_tracker(self):
|
|
448
727
|
"""Serve tracker items as JSON via GET /api/tracker."""
|
|
@@ -460,11 +739,16 @@ class DashboardHandler(http.server.BaseHTTPRequestHandler):
|
|
|
460
739
|
self.end_headers()
|
|
461
740
|
self.wfile.write(json.dumps(body, default=str).encode('utf-8'))
|
|
462
741
|
except Exception as e:
|
|
742
|
+
if _is_client_disconnect_error(e):
|
|
743
|
+
return # client gone: nothing to send or log
|
|
463
744
|
print(f"[serve_tracker] Uncaught exception: {e}", file=sys.stderr)
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
745
|
+
try:
|
|
746
|
+
self.send_response(500)
|
|
747
|
+
self.send_header("Content-Type", "application/json")
|
|
748
|
+
self.end_headers()
|
|
749
|
+
self.wfile.write(json.dumps({"error": "Internal server error"}).encode('utf-8'))
|
|
750
|
+
except Exception:
|
|
751
|
+
pass # best-effort error response; client may be gone
|
|
468
752
|
|
|
469
753
|
def handle_tracker_create(self):
|
|
470
754
|
"""Handle POST /api/tracker (create item)."""
|
|
@@ -485,12 +769,19 @@ class DashboardHandler(http.server.BaseHTTPRequestHandler):
|
|
|
485
769
|
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
486
770
|
self.end_headers()
|
|
487
771
|
self.wfile.write(json.dumps(result, default=str).encode('utf-8'))
|
|
772
|
+
except (BrokenPipeError, ConnectionAbortedError, ConnectionResetError):
|
|
773
|
+
pass # Client disconnected — normal lifecycle, not an error.
|
|
488
774
|
except Exception as e:
|
|
775
|
+
if _is_client_disconnect_error(e):
|
|
776
|
+
return # client gone: nothing to send or log
|
|
489
777
|
print(f"[handle_tracker_create] Uncaught exception: {e}", file=sys.stderr)
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
778
|
+
try:
|
|
779
|
+
self.send_response(500)
|
|
780
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
781
|
+
self.end_headers()
|
|
782
|
+
self.wfile.write(json.dumps({"error": "Internal server error"}).encode('utf-8'))
|
|
783
|
+
except Exception:
|
|
784
|
+
pass # best-effort error response; client may be gone
|
|
494
785
|
|
|
495
786
|
def handle_tracker_mutate(self):
|
|
496
787
|
"""Handle POST /api/tracker/<id> (update or delete)."""
|
|
@@ -534,12 +825,19 @@ class DashboardHandler(http.server.BaseHTTPRequestHandler):
|
|
|
534
825
|
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
535
826
|
self.end_headers()
|
|
536
827
|
self.wfile.write(json.dumps(result, default=str).encode('utf-8'))
|
|
828
|
+
except (BrokenPipeError, ConnectionAbortedError, ConnectionResetError):
|
|
829
|
+
pass # Client disconnected — normal lifecycle, not an error.
|
|
537
830
|
except Exception as e:
|
|
831
|
+
if _is_client_disconnect_error(e):
|
|
832
|
+
return # client gone: nothing to send or log
|
|
538
833
|
print(f"[handle_tracker_mutate] Uncaught exception: {e}", file=sys.stderr)
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
834
|
+
try:
|
|
835
|
+
self.send_response(500)
|
|
836
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
837
|
+
self.end_headers()
|
|
838
|
+
self.wfile.write(json.dumps({"error": "Internal server error"}).encode('utf-8'))
|
|
839
|
+
except Exception:
|
|
840
|
+
pass # best-effort error response; client may be gone
|
|
543
841
|
|
|
544
842
|
|
|
545
843
|
def serve_backlog(self):
|
|
@@ -552,11 +850,16 @@ class DashboardHandler(http.server.BaseHTTPRequestHandler):
|
|
|
552
850
|
self.end_headers()
|
|
553
851
|
self.wfile.write(json.dumps(data, default=str).encode('utf-8'))
|
|
554
852
|
except Exception as e:
|
|
853
|
+
if _is_client_disconnect_error(e):
|
|
854
|
+
return # client gone: nothing to send or log
|
|
555
855
|
print(f"[serve_backlog] Uncaught exception: {e}", file=sys.stderr)
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
856
|
+
try:
|
|
857
|
+
self.send_response(500)
|
|
858
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
859
|
+
self.end_headers()
|
|
860
|
+
self.wfile.write(json.dumps({"error": "Internal server error"}).encode('utf-8'))
|
|
861
|
+
except Exception:
|
|
862
|
+
pass # best-effort error response; client may be gone
|
|
560
863
|
|
|
561
864
|
def serve_agents(self):
|
|
562
865
|
"""Serve rich agent list with metadata via GET /api/agents."""
|
|
@@ -568,11 +871,16 @@ class DashboardHandler(http.server.BaseHTTPRequestHandler):
|
|
|
568
871
|
self.end_headers()
|
|
569
872
|
self.wfile.write(json.dumps(agents, default=str).encode('utf-8'))
|
|
570
873
|
except Exception as e:
|
|
874
|
+
if _is_client_disconnect_error(e):
|
|
875
|
+
return # client gone: nothing to send or log
|
|
571
876
|
print(f"[serve_agents] Uncaught exception: {e}", file=sys.stderr)
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
877
|
+
try:
|
|
878
|
+
self.send_response(500)
|
|
879
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
880
|
+
self.end_headers()
|
|
881
|
+
self.wfile.write(json.dumps({"error": "Internal server error"}).encode('utf-8'))
|
|
882
|
+
except Exception:
|
|
883
|
+
pass # best-effort error response; client may be gone
|
|
576
884
|
|
|
577
885
|
def serve_agent(self):
|
|
578
886
|
"""Serve agent dispatch prompt and metadata via GET /agent?id=<agent_id>"""
|
|
@@ -612,7 +920,8 @@ class DashboardHandler(http.server.BaseHTTPRequestHandler):
|
|
|
612
920
|
self.send_response(500)
|
|
613
921
|
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
614
922
|
self.end_headers()
|
|
615
|
-
|
|
923
|
+
if not _is_client_disconnect_error(e):
|
|
924
|
+
print(f"[serve_agent] Uncaught exception: {e}", file=sys.stderr)
|
|
616
925
|
self.wfile.write(json.dumps({"error": "Internal server error"}).encode('utf-8'))
|
|
617
926
|
|
|
618
927
|
def serve_api_agent(self):
|
|
@@ -655,7 +964,8 @@ class DashboardHandler(http.server.BaseHTTPRequestHandler):
|
|
|
655
964
|
self.send_response(500)
|
|
656
965
|
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
657
966
|
self.end_headers()
|
|
658
|
-
|
|
967
|
+
if not _is_client_disconnect_error(e):
|
|
968
|
+
print(f"[serve_api_agent] Uncaught exception: {e}", file=sys.stderr)
|
|
659
969
|
self.wfile.write(json.dumps({"error": "Internal server error"}).encode('utf-8'))
|
|
660
970
|
|
|
661
971
|
def _write_sse_event(self, event_name, payload):
|
|
@@ -700,8 +1010,12 @@ class DashboardHandler(http.server.BaseHTTPRequestHandler):
|
|
|
700
1010
|
self.send_header("Retry-After", "30")
|
|
701
1011
|
self.end_headers()
|
|
702
1012
|
self.wfile.write(b"Service overloaded: too many concurrent clients\n")
|
|
703
|
-
except (BrokenPipeError, ConnectionAbortedError, ConnectionResetError
|
|
1013
|
+
except (BrokenPipeError, ConnectionAbortedError, ConnectionResetError):
|
|
704
1014
|
pass
|
|
1015
|
+
except OSError as e:
|
|
1016
|
+
if not _is_client_disconnect_error(e):
|
|
1017
|
+
print(f"[serve_events cap-exceeded] Uncaught OSError: {e}", file=sys.stderr)
|
|
1018
|
+
# else: silently pass for disconnect errors
|
|
705
1019
|
return
|
|
706
1020
|
|
|
707
1021
|
try:
|
|
@@ -711,7 +1025,12 @@ class DashboardHandler(http.server.BaseHTTPRequestHandler):
|
|
|
711
1025
|
self.send_header("Connection", "keep-alive")
|
|
712
1026
|
self.send_header("X-Accel-Buffering", "no")
|
|
713
1027
|
self.end_headers()
|
|
714
|
-
except (BrokenPipeError, ConnectionAbortedError, ConnectionResetError
|
|
1028
|
+
except (BrokenPipeError, ConnectionAbortedError, ConnectionResetError):
|
|
1029
|
+
unregister_sse_client(q)
|
|
1030
|
+
return
|
|
1031
|
+
except OSError as e:
|
|
1032
|
+
if not _is_client_disconnect_error(e):
|
|
1033
|
+
print(f"[serve_events headers] Uncaught OSError: {e}", file=sys.stderr)
|
|
715
1034
|
unregister_sse_client(q)
|
|
716
1035
|
return
|
|
717
1036
|
try:
|
|
@@ -742,9 +1061,13 @@ class DashboardHandler(http.server.BaseHTTPRequestHandler):
|
|
|
742
1061
|
except queue.Empty:
|
|
743
1062
|
self.wfile.write(b": keepalive\n\n")
|
|
744
1063
|
self.wfile.flush()
|
|
745
|
-
except (BrokenPipeError, ConnectionAbortedError, ConnectionResetError
|
|
1064
|
+
except (BrokenPipeError, ConnectionAbortedError, ConnectionResetError):
|
|
746
1065
|
# Client disconnected (tab closed, network drop) — normal, not an error.
|
|
747
1066
|
pass
|
|
1067
|
+
except OSError as e:
|
|
1068
|
+
if not _is_client_disconnect_error(e):
|
|
1069
|
+
print(f"[serve_events stream] Uncaught OSError: {e}", file=sys.stderr)
|
|
1070
|
+
# else: silently pass for disconnect errors
|
|
748
1071
|
except Exception:
|
|
749
1072
|
pass
|
|
750
1073
|
finally:
|
|
@@ -799,11 +1122,16 @@ class DashboardHandler(http.server.BaseHTTPRequestHandler):
|
|
|
799
1122
|
self.end_headers()
|
|
800
1123
|
self.wfile.write(json.dumps({"ok": True}).encode('utf-8'))
|
|
801
1124
|
except Exception as e:
|
|
1125
|
+
if _is_client_disconnect_error(e):
|
|
1126
|
+
return # client gone: nothing to send or log
|
|
802
1127
|
print(f"[handle_submit] Uncaught exception: {e}", file=sys.stderr)
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
1128
|
+
try:
|
|
1129
|
+
self.send_response(500)
|
|
1130
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
1131
|
+
self.end_headers()
|
|
1132
|
+
self.wfile.write(json.dumps({"error": "Internal server error"}).encode('utf-8'))
|
|
1133
|
+
except Exception:
|
|
1134
|
+
pass # best-effort error response; client may be gone
|
|
807
1135
|
|
|
808
1136
|
|
|
809
1137
|
class QuietThreadingHTTPServer(http.server.ThreadingHTTPServer):
|