@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.
Files changed (114) hide show
  1. package/CHANGELOG.md +89 -2
  2. package/README.md +63 -12
  3. package/bin/cli.js +164 -41
  4. package/daemons/run-watchdog.sh +16 -4
  5. package/daemons/selfheal.sh +231 -0
  6. package/docs/ANY-REPO.md +427 -0
  7. package/docs/CONTRIBUTING.md +72 -0
  8. package/docs/DEMO.md +334 -0
  9. package/docs/INSTALL.md +70 -1
  10. package/docs/QUICKSTART.md +80 -0
  11. package/docs/README.md +33 -3
  12. package/docs/TEAM-STATE.md +540 -0
  13. package/driver/CLAUDE.md +148 -0
  14. package/driver/README.md +383 -0
  15. package/driver/aesop.config.example.json +80 -0
  16. package/driver/agent_driver.py +355 -0
  17. package/driver/backend_config.py +253 -0
  18. package/driver/claude_code_driver.py +198 -0
  19. package/driver/codex_driver.py +626 -0
  20. package/driver/openai_compatible_driver.py +249 -0
  21. package/driver/openai_transport.py +146 -0
  22. package/driver/verification_policy.py +75 -0
  23. package/driver/wave_bridge.py +246 -0
  24. package/driver/wave_loop.py +1041 -0
  25. package/hooks/pre-push-policy.sh +109 -28
  26. package/mcp/server.mjs +320 -4
  27. package/package.json +23 -15
  28. package/skills/CLAUDE.md +132 -2
  29. package/skills/buildsystem/SKILL.md +330 -0
  30. package/skills/buildsystem/wave-flat-dispatch.template.mjs +658 -0
  31. package/skills/fleet/SKILL.md +113 -0
  32. package/skills/power/SKILL.md +246 -131
  33. package/state_store/__init__.py +2 -1
  34. package/state_store/api.py +19 -4
  35. package/state_store/coordination.py +209 -0
  36. package/state_store/identity.py +51 -0
  37. package/state_store/store.py +185 -73
  38. package/templates/wave-presets/data.json +65 -0
  39. package/templates/wave-presets/library.json +65 -0
  40. package/templates/wave-presets/saas.json +64 -0
  41. package/tools/audit_report.py +388 -0
  42. package/tools/bench_runner.py +100 -3
  43. package/tools/ci_merge_wait.py +256 -35
  44. package/tools/ci_workflow_lint.py +430 -0
  45. package/tools/claudemd_drift.py +394 -0
  46. package/tools/claudemd_lint.py +359 -0
  47. package/tools/common.py +39 -3
  48. package/tools/cost_ceiling.py +63 -31
  49. package/tools/cost_econ.py +480 -0
  50. package/tools/defect_escape.py +252 -0
  51. package/tools/doctor.js +1 -1
  52. package/tools/eod_sweep.py +58 -0
  53. package/tools/fleet.js +260 -0
  54. package/tools/fleet_ledger.py +209 -7
  55. package/tools/git_identity_check.py +315 -0
  56. package/tools/health-score.js +40 -0
  57. package/tools/health_score.py +361 -0
  58. package/tools/metrics_gate.py +13 -4
  59. package/tools/mutation_test.py +401 -0
  60. package/tools/portability_check.py +206 -0
  61. package/tools/reconcile.py +7 -4
  62. package/tools/secret_scan.py +137 -50
  63. package/tools/self_stats.py +20 -0
  64. package/tools/transcript_digest.py +380 -0
  65. package/tools/verify_activity_filter.py +437 -0
  66. package/tools/verify_agent_inspector.py +2 -0
  67. package/tools/verify_dash.py +2 -0
  68. package/tools/verify_dispatch_panel.py +301 -0
  69. package/tools/verify_failure_drilldown.py +188 -0
  70. package/tools/verify_prboard.py +2 -0
  71. package/tools/verify_scorecards.py +281 -0
  72. package/tools/verify_submit_encoding.py +2 -0
  73. package/tools/verify_ui_trio.py +409 -0
  74. package/tools/verify_wave_telemetry.py +268 -0
  75. package/tools/wave_backlog_analyzer.py +490 -0
  76. package/tools/wave_ledger_hook.py +150 -0
  77. package/tools/wave_preflight.py +512 -0
  78. package/tools/wave_resume.py +215 -0
  79. package/tools/wave_templates.py +215 -0
  80. package/ui/agents.py +68 -14
  81. package/ui/collectors.py +0 -55
  82. package/ui/config.py +7 -2
  83. package/ui/cost.py +231 -12
  84. package/ui/handler.py +183 -0
  85. package/ui/quality_scorecard.py +232 -0
  86. package/ui/wave_audit_tail.py +213 -0
  87. package/ui/wave_dispatch.py +280 -0
  88. package/ui/wave_failure.py +288 -0
  89. package/ui/wave_gantt.py +152 -0
  90. package/ui/wave_reasoning_tail.py +176 -0
  91. package/ui/wave_telemetry.py +377 -0
  92. package/ui/web/dist/assets/index-BGbgw2Nh.js +9 -0
  93. package/ui/web/dist/assets/index-DqZLgwNg.css +1 -0
  94. package/ui/web/dist/index.html +2 -2
  95. package/bin/CLAUDE.md +0 -76
  96. package/daemons/CLAUDE.md +0 -36
  97. package/dash/CLAUDE.md +0 -32
  98. package/docs/archive/README.md +0 -3
  99. package/docs/archive/spikes/tiered-cognition/ACTIVATION.md +0 -125
  100. package/docs/archive/spikes/tiered-cognition/DESIGN.md +0 -287
  101. package/docs/archive/spikes/tiered-cognition/FINDINGS.md +0 -113
  102. package/docs/archive/spikes/tiered-cognition/README.md +0 -27
  103. package/docs/archive/spikes/tiered-cognition/aesop-cognition.example.md +0 -32
  104. package/docs/archive/spikes/tiered-cognition/force-model-policy.merged.mjs +0 -673
  105. package/docs/archive/spikes/tiered-cognition/strip-tools-hook.mjs +0 -434
  106. package/hooks/CLAUDE.md +0 -89
  107. package/mcp/CLAUDE.md +0 -213
  108. package/monitor/CLAUDE.md +0 -40
  109. package/scan/CLAUDE.md +0 -30
  110. package/state_store/CLAUDE.md +0 -39
  111. package/tools/CLAUDE.md +0 -79
  112. package/ui/CLAUDE.md +0 -127
  113. package/ui/web/dist/assets/index-0qQYnvMC.js +0 -9
  114. package/ui/web/dist/assets/index-BdIlFieV.css +0 -1
@@ -0,0 +1,377 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Wave telemetry collector — current wave phase, cost metrics, and blockers.
4
+
5
+ Reads state at CALL TIME (not import time) to ensure test isolation.
6
+ State sources:
7
+ - state/orchestrator-status.json: current phase and activity (preferred, <24h)
8
+ - STATE.md: current phase and wave info (fallback if status file missing/stale)
9
+ - AUDIT-BACKLOG.md: top blocker via parse
10
+ - state/ledger/OUTCOMES-LEDGER.md: cost data (re-uses cost.py logic)
11
+ """
12
+ import json
13
+ import re
14
+ import sys
15
+ from datetime import datetime, timezone, timedelta
16
+ from pathlib import Path
17
+
18
+ import config
19
+ import cost
20
+
21
+
22
+ def _read_orchestrator_status():
23
+ """Read orchestrator-status.json if fresh (<24h old).
24
+
25
+ Returns:
26
+ tuple: (phase_str, activity_str, source_str) or (None, None, None) if missing/stale/malformed
27
+ """
28
+ try:
29
+ status_file = config.ORCH_STATUS_FILE
30
+ if not status_file.exists():
31
+ return None, None, None
32
+
33
+ content = status_file.read_text(encoding='utf-8')
34
+ data = json.loads(content)
35
+
36
+ # Extract updated_at and check freshness
37
+ updated_at_str = data.get("updated_at")
38
+ if not updated_at_str:
39
+ return None, None, None
40
+
41
+ # Parse ISO format timestamp (handle both "Z" and "+00:00" suffixes)
42
+ updated_at_str_normalized = updated_at_str.replace("Z", "+00:00")
43
+ try:
44
+ updated_at = datetime.fromisoformat(updated_at_str_normalized)
45
+ except ValueError:
46
+ return None, None, None
47
+
48
+ # Check if fresh (<24h) and not in the future
49
+ now = datetime.now(timezone.utc)
50
+ age = now - updated_at
51
+ # Treat ANY future-dated timestamp as NOT fresh (fail-closed)
52
+ if age > timedelta(hours=24) or age < timedelta(0):
53
+ return None, None, None
54
+
55
+ # Extract phase and activity
56
+ phase = data.get("phase", "").lower()
57
+ activity = data.get("activity", "").lower()
58
+
59
+ if not phase:
60
+ return None, None, None
61
+
62
+ return phase, activity, "orchestrator-status"
63
+ except Exception as e:
64
+ print(f"[wave_telemetry] Error reading orchestrator-status.json: {e}", file=sys.stderr)
65
+ return None, None, None
66
+
67
+
68
+ def _parse_state_md_phase():
69
+ """Extract current phase and wave from STATE.md.
70
+
71
+ Looks for lines like:
72
+ ## Phase: `rc-1-published-source-available` (2026-07-17, current)
73
+ or
74
+ ## Phase: `wave-rc.2` (2026-07-17, current)
75
+
76
+ Returns:
77
+ dict: {"wave": str, "phase": str} or {"wave": "unknown", "phase": "unknown"}
78
+ """
79
+ try:
80
+ state_md = config.AESOP_ROOT / "STATE.md"
81
+ if not state_md.exists():
82
+ return {"wave": "unknown", "phase": "unknown"}
83
+
84
+ content = state_md.read_text(encoding='utf-8')
85
+
86
+ # Look for ## Phase: `...` (..., current)
87
+ # Extract the phase name between backticks and the wave indicator
88
+ match = re.search(r'## Phase:\s*`([^`]+)`', content)
89
+ if not match:
90
+ return {"wave": "unknown", "phase": "unknown"}
91
+
92
+ phase = match.group(1)
93
+
94
+ # Extract wave name from phase (e.g., "wave-rc.2" from "rc-1-published-source-available")
95
+ # Match patterns like "wave-26", "wave-rc.2", "rc-1", etc.
96
+ # Use (?:\.\w+)* to capture dot-separated identifiers (e.g., "wave-rc.2")
97
+ wave_match = re.search(r'(wave[-.]?\w+(?:\.\w+)*|rc[-.]?\w+(?:\.\w+)*)', phase, re.IGNORECASE)
98
+ if wave_match:
99
+ wave_str = wave_match.group(0) # e.g., "wave-26" or "rc-1"
100
+ else:
101
+ wave_str = phase # Fallback to phase itself
102
+
103
+ return {
104
+ "wave": wave_str.lower(),
105
+ "phase": phase.lower()
106
+ }
107
+ except Exception as e:
108
+ print(f"[wave_telemetry] Error parsing STATE.md: {e}", file=sys.stderr)
109
+ return {"wave": "unknown", "phase": "unknown"}
110
+
111
+
112
+ def _parse_top_blocker():
113
+ """Extract top blocker from AUDIT-BACKLOG.md.
114
+
115
+ Looks for the first P0 item with status 🔵 (inflight) or ⬜ (todo).
116
+ Returns the title if found, otherwise looks for orchestrator-status.
117
+
118
+ Returns:
119
+ str: blocker title or reason, e.g., "CI test flakes", "unknown"
120
+ """
121
+ try:
122
+ backlog_file = config.AUDIT_BACKLOG_FILE
123
+ if not backlog_file.exists():
124
+ return "unknown"
125
+
126
+ content = backlog_file.read_text(encoding='utf-8')
127
+ lines = content.split('\n')
128
+
129
+ in_p0 = False
130
+ for line in lines:
131
+ line_stripped = line.strip()
132
+
133
+ # Check if we entered P0 section
134
+ if re.match(r'^##\s*P0\b', line_stripped):
135
+ in_p0 = True
136
+ continue
137
+
138
+ # If we entered a different tier, stop
139
+ if line_stripped.startswith('## ') and not re.match(r'^##\s*P0\b', line_stripped):
140
+ in_p0 = False
141
+ continue
142
+
143
+ if in_p0 and line_stripped.startswith('- '):
144
+ # Check for inflight (🔵) or todo (⬜) status
145
+ if '🔵' in line_stripped or '⬜' in line_stripped:
146
+ # Extract title after status
147
+ # Pattern: - 🔵 **[tag] Title**
148
+ match = re.search(r'(?:🔵|⬜)\s+\*\*\[([^\]]+)\]\s+(.+?)\*\*', line_stripped)
149
+ if match:
150
+ title = match.group(2)
151
+ return title
152
+
153
+ return "unknown"
154
+ except Exception as e:
155
+ print(f"[wave_telemetry] Error parsing blocker: {e}", file=sys.stderr)
156
+ return "unknown"
157
+
158
+
159
+ def _get_wave_cost_metrics():
160
+ """Get cost metrics for the current wave.
161
+
162
+ Uses cost.py to parse the ledger and return this-wave totals.
163
+ For now, returns a simplified snapshot of per-model tokens and OK rate.
164
+
165
+ Returns:
166
+ dict: {
167
+ "tokens_used": int,
168
+ "top_model": str,
169
+ "ok_rate": float (0.0-1.0)
170
+ }
171
+ """
172
+ try:
173
+ summary = cost.get_cost_summary()
174
+
175
+ if "error" in summary:
176
+ return {
177
+ "tokens_used": 0,
178
+ "top_model": "unknown",
179
+ "ok_rate": 0.0
180
+ }
181
+
182
+ # Sum total tokens across all models
183
+ total_tokens = 0
184
+ top_model = "unknown"
185
+ top_model_tokens = 0
186
+
187
+ for model, stats in summary.get("models", {}).items():
188
+ tokens = stats.get("tokens_in", 0) + stats.get("tokens_out", 0)
189
+ total_tokens += tokens
190
+ if tokens > top_model_tokens:
191
+ # Extract model name (e.g., "haiku" from "claude-haiku-4-5" or "claude-haiku-4-5-20251001")
192
+ model_lower = model.lower()
193
+ if "haiku" in model_lower:
194
+ top_model = "haiku"
195
+ elif "sonnet" in model_lower:
196
+ top_model = "sonnet"
197
+ elif "opus" in model_lower:
198
+ top_model = "opus"
199
+ else:
200
+ # Fallback to last part of model id
201
+ top_model = model.split("-")[-1]
202
+ top_model_tokens = tokens
203
+
204
+ scorecard = summary.get("overall_scorecard", {})
205
+ ok_rate = scorecard.get("ok_rate", 0.0)
206
+
207
+ return {
208
+ "tokens_used": total_tokens,
209
+ "top_model": top_model,
210
+ "ok_rate": ok_rate
211
+ }
212
+ except Exception as e:
213
+ print(f"[wave_telemetry] Error computing cost metrics: {e}", file=sys.stderr)
214
+ return {
215
+ "tokens_used": 0,
216
+ "top_model": "unknown",
217
+ "ok_rate": 0.0
218
+ }
219
+
220
+
221
+ def _get_wave_start_time():
222
+ """Get the wave start time from orchestrator-status.json or STATE.md.
223
+
224
+ Returns:
225
+ datetime or None: wave start time in UTC, or None if not available
226
+ """
227
+ try:
228
+ status_file = config.ORCH_STATUS_FILE
229
+ if status_file.exists():
230
+ content = status_file.read_text(encoding='utf-8')
231
+ data = json.loads(content)
232
+ # Check if there's a wave_start_time or started_at field
233
+ start_time_str = data.get("wave_start_time") or data.get("started_at")
234
+ if start_time_str:
235
+ start_time_str_normalized = start_time_str.replace("Z", "+00:00")
236
+ return datetime.fromisoformat(start_time_str_normalized)
237
+ except Exception:
238
+ pass
239
+ return None
240
+
241
+
242
+ def _calculate_burn_rate_fields(total_tokens: int) -> dict:
243
+ """Calculate burn-rate and projection fields for live wave cost.
244
+
245
+ Args:
246
+ total_tokens: total tokens burned in this wave so far
247
+
248
+ Returns:
249
+ dict: {
250
+ "tokens_burned_per_min": float (tokens/min),
251
+ "projected_total_tokens": int (at current rate),
252
+ "cost_ceiling_exceeded": bool
253
+ }
254
+ """
255
+ try:
256
+ # Try to get wave start time for burn-rate calculation
257
+ wave_start = _get_wave_start_time()
258
+ if not wave_start:
259
+ # Fallback: estimate from cost ledger timestamps if available
260
+ return {
261
+ "tokens_burned_per_min": 0.0,
262
+ "projected_total_tokens": 0,
263
+ "cost_ceiling_exceeded": False
264
+ }
265
+
266
+ now = datetime.now(timezone.utc)
267
+ elapsed_seconds = (now - wave_start).total_seconds()
268
+ if elapsed_seconds < 1:
269
+ return {
270
+ "tokens_burned_per_min": 0.0,
271
+ "projected_total_tokens": 0,
272
+ "cost_ceiling_exceeded": False
273
+ }
274
+
275
+ elapsed_minutes = elapsed_seconds / 60.0
276
+ burn_rate = total_tokens / elapsed_minutes if elapsed_minutes > 0 else 0.0
277
+
278
+ # Get cost ceiling from config (typical: 2M tokens = $40 budget estimate)
279
+ cost_ceiling = getattr(config, 'COST_CEILING_TOKENS', 2_000_000)
280
+
281
+ # Estimate wave duration (typical: 8-30 min, assume 20 min average for projection)
282
+ avg_wave_duration_min = 20
283
+ estimated_remaining_min = max(0, avg_wave_duration_min - elapsed_minutes)
284
+ projected_additional = burn_rate * estimated_remaining_min
285
+ projected_total = int(total_tokens + projected_additional)
286
+
287
+ ceiling_exceeded = total_tokens > cost_ceiling
288
+
289
+ return {
290
+ "tokens_burned_per_min": round(burn_rate, 1),
291
+ "projected_total_tokens": projected_total,
292
+ "cost_ceiling_exceeded": ceiling_exceeded
293
+ }
294
+ except Exception as e:
295
+ print(f"[wave_telemetry] Error calculating burn-rate: {e}", file=sys.stderr)
296
+ return {
297
+ "tokens_burned_per_min": 0.0,
298
+ "projected_total_tokens": 0,
299
+ "cost_ceiling_exceeded": False
300
+ }
301
+
302
+
303
+ def get_wave_telemetry():
304
+ """Get consolidated wave telemetry snapshot.
305
+
306
+ Prefers state/orchestrator-status.json (if fresh <24h) over STATE.md regex parsing.
307
+ Reads all state at call time (not import time) to ensure test isolation.
308
+
309
+ Returns:
310
+ dict: {
311
+ "wave": str,
312
+ "phase": str,
313
+ "blocker": str,
314
+ "tokens_used": int,
315
+ "top_model": str,
316
+ "ok_rate": float,
317
+ "source": "orchestrator-status" | "state-md",
318
+ "tokens_burned_per_min": float (NEW: burn rate),
319
+ "projected_total_tokens": int (NEW: projection),
320
+ "cost_ceiling_exceeded": bool (NEW: alert flag)
321
+ }
322
+ """
323
+ try:
324
+ # Try orchestrator-status.json first (fresh <24h)
325
+ orch_phase, orch_activity, source = _read_orchestrator_status()
326
+
327
+ if orch_phase:
328
+ # Fresh orchestrator-status.json found; use it
329
+ # Extract wave identifier from phase (e.g., "wave-26" from "wave-26-verify", "wave-rc.2" from "wave-rc.2: build")
330
+ # Use (?:\.\w+)* to capture dot-separated identifiers (e.g., "wave-rc.2")
331
+ wave_match = re.search(r'(wave[-.]?\w+(?:\.\w+)*|rc[-.]?\w+(?:\.\w+)*)', orch_phase, re.IGNORECASE)
332
+ if wave_match:
333
+ wave_str = wave_match.group(0) # e.g., "wave-26", "wave-rc", or "rc-1"
334
+ else:
335
+ wave_str = orch_phase # Fallback to phase itself
336
+ phase_info = {
337
+ "wave": wave_str.lower(),
338
+ "phase": orch_phase
339
+ }
340
+ source_field = source
341
+ else:
342
+ # Fall back to STATE.md
343
+ phase_info = _parse_state_md_phase()
344
+ source_field = "state-md"
345
+
346
+ blocker = _parse_top_blocker()
347
+ cost_metrics = _get_wave_cost_metrics()
348
+
349
+ # Calculate burn-rate fields
350
+ burn_rate_fields = _calculate_burn_rate_fields(cost_metrics["tokens_used"])
351
+
352
+ return {
353
+ "wave": phase_info["wave"],
354
+ "phase": phase_info["phase"],
355
+ "blocker": blocker,
356
+ "tokens_used": cost_metrics["tokens_used"],
357
+ "top_model": cost_metrics["top_model"],
358
+ "ok_rate": cost_metrics["ok_rate"],
359
+ "source": source_field,
360
+ "tokens_burned_per_min": burn_rate_fields["tokens_burned_per_min"],
361
+ "projected_total_tokens": burn_rate_fields["projected_total_tokens"],
362
+ "cost_ceiling_exceeded": burn_rate_fields["cost_ceiling_exceeded"]
363
+ }
364
+ except Exception as e:
365
+ print(f"[wave_telemetry] Uncaught error: {e}", file=sys.stderr)
366
+ return {
367
+ "wave": "unknown",
368
+ "phase": "unknown",
369
+ "blocker": "error",
370
+ "tokens_used": 0,
371
+ "top_model": "unknown",
372
+ "ok_rate": 0.0,
373
+ "source": "error",
374
+ "tokens_burned_per_min": 0.0,
375
+ "projected_total_tokens": 0,
376
+ "cost_ceiling_exceeded": False
377
+ }