@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,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
|
+
}
|