@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.
Files changed (130) hide show
  1. package/CHANGELOG.md +117 -2
  2. package/README.md +59 -218
  3. package/bin/cli.js +168 -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/HOOK-INSTALL.md +15 -56
  10. package/docs/INSTALL.md +74 -4
  11. package/docs/PORTING.md +166 -0
  12. package/docs/README.md +33 -3
  13. package/docs/TEAM-STATE.md +372 -0
  14. package/docs/reproduce.md +33 -3
  15. package/driver/CLAUDE.md +150 -0
  16. package/driver/README.md +383 -0
  17. package/driver/aesop.config.example.json +80 -0
  18. package/driver/agent_driver.py +355 -0
  19. package/driver/backend_config.py +253 -0
  20. package/driver/claude_code_driver.py +198 -0
  21. package/driver/codex_driver.py +673 -0
  22. package/driver/openai_compatible_driver.py +249 -0
  23. package/driver/openai_transport.py +179 -0
  24. package/driver/verification_policy.py +75 -0
  25. package/driver/wave_bridge.py +254 -0
  26. package/driver/wave_loop.py +1408 -0
  27. package/driver/wave_scheduler.py +890 -0
  28. package/hooks/pre-push-policy.sh +131 -33
  29. package/mcp/server.mjs +320 -4
  30. package/monitor/collect-signals.mjs +69 -0
  31. package/package.json +22 -14
  32. package/skills/CLAUDE.md +132 -2
  33. package/skills/buildsystem/SKILL.md +330 -0
  34. package/skills/buildsystem/wave-flat-dispatch.template.mjs +658 -0
  35. package/skills/fleet/SKILL.md +113 -0
  36. package/skills/power/SKILL.md +246 -131
  37. package/state_store/__init__.py +4 -2
  38. package/state_store/api.py +19 -4
  39. package/state_store/coordination.py +209 -0
  40. package/state_store/identity.py +51 -0
  41. package/state_store/projections.py +63 -0
  42. package/state_store/read_api.py +156 -0
  43. package/state_store/store.py +185 -73
  44. package/state_store/write_api.py +462 -0
  45. package/templates/wave-presets/data.json +65 -0
  46. package/templates/wave-presets/library.json +65 -0
  47. package/templates/wave-presets/saas.json +64 -0
  48. package/tools/audit_report.py +388 -0
  49. package/tools/bench_runner.py +100 -3
  50. package/tools/ci_merge_wait.py +256 -35
  51. package/tools/ci_workflow_lint.py +430 -0
  52. package/tools/claudemd_drift.py +394 -0
  53. package/tools/claudemd_lint.py +359 -0
  54. package/tools/common.py +39 -3
  55. package/tools/cost_ceiling.py +166 -43
  56. package/tools/cost_econ.py +480 -0
  57. package/tools/cost_projection.py +559 -0
  58. package/tools/crossos_drift.py +394 -0
  59. package/tools/defect_escape.py +252 -0
  60. package/tools/doctor.js +1 -1
  61. package/tools/eod_sweep.py +188 -26
  62. package/tools/fleet.js +260 -0
  63. package/tools/fleet_ledger.py +209 -7
  64. package/tools/git_identity_check.py +315 -0
  65. package/tools/health-score.js +40 -0
  66. package/tools/health_score.py +361 -0
  67. package/tools/metrics_gate.py +13 -4
  68. package/tools/mutation_test.py +523 -0
  69. package/tools/portability_check.py +206 -0
  70. package/tools/proposals.mjs +47 -2
  71. package/tools/reconcile.py +7 -4
  72. package/tools/reproduce.js +405 -0
  73. package/tools/secret_scan.py +207 -65
  74. package/tools/self_stats.py +20 -0
  75. package/tools/stall_check.py +247 -16
  76. package/tools/stateapi_lint.py +325 -0
  77. package/tools/test_battery.py +173 -0
  78. package/tools/transcript_digest.py +380 -0
  79. package/tools/verify_activity_filter.py +437 -0
  80. package/tools/verify_agent_inspector.py +2 -0
  81. package/tools/verify_cost_panel.py +345 -0
  82. package/tools/verify_dash.py +2 -0
  83. package/tools/verify_dispatch_panel.py +301 -0
  84. package/tools/verify_failure_drilldown.py +188 -0
  85. package/tools/verify_prboard.py +2 -0
  86. package/tools/verify_scorecards.py +281 -0
  87. package/tools/verify_submit_encoding.py +2 -0
  88. package/tools/verify_ui_trio.py +409 -0
  89. package/tools/verify_wave_telemetry.py +268 -0
  90. package/tools/wave_backlog_analyzer.py +490 -0
  91. package/tools/wave_ledger_hook.py +150 -0
  92. package/tools/wave_preflight.py +779 -0
  93. package/tools/wave_resume.py +215 -0
  94. package/tools/wave_templates.py +340 -0
  95. package/ui/agents.py +68 -14
  96. package/ui/collectors.py +81 -55
  97. package/ui/config.py +7 -2
  98. package/ui/cost.py +231 -12
  99. package/ui/handler.py +383 -55
  100. package/ui/quality_scorecard.py +232 -0
  101. package/ui/sse.py +3 -3
  102. package/ui/wave_audit_tail.py +213 -0
  103. package/ui/wave_dispatch.py +280 -0
  104. package/ui/wave_failure.py +288 -0
  105. package/ui/wave_gantt.py +152 -0
  106. package/ui/wave_reasoning_tail.py +176 -0
  107. package/ui/wave_telemetry.py +383 -0
  108. package/ui/web/dist/assets/index-CNQxaiOW.css +1 -0
  109. package/ui/web/dist/assets/index-CP68RIh3.js +9 -0
  110. package/ui/web/dist/index.html +2 -2
  111. package/bin/CLAUDE.md +0 -76
  112. package/daemons/CLAUDE.md +0 -36
  113. package/dash/CLAUDE.md +0 -32
  114. package/docs/archive/README.md +0 -3
  115. package/docs/archive/spikes/tiered-cognition/ACTIVATION.md +0 -125
  116. package/docs/archive/spikes/tiered-cognition/DESIGN.md +0 -287
  117. package/docs/archive/spikes/tiered-cognition/FINDINGS.md +0 -113
  118. package/docs/archive/spikes/tiered-cognition/README.md +0 -27
  119. package/docs/archive/spikes/tiered-cognition/aesop-cognition.example.md +0 -32
  120. package/docs/archive/spikes/tiered-cognition/force-model-policy.merged.mjs +0 -673
  121. package/docs/archive/spikes/tiered-cognition/strip-tools-hook.mjs +0 -434
  122. package/hooks/CLAUDE.md +0 -89
  123. package/mcp/CLAUDE.md +0 -213
  124. package/monitor/CLAUDE.md +0 -40
  125. package/scan/CLAUDE.md +0 -30
  126. package/state_store/CLAUDE.md +0 -39
  127. package/tools/CLAUDE.md +0 -79
  128. package/ui/CLAUDE.md +0 -127
  129. package/ui/web/dist/assets/index-0qQYnvMC.js +0 -9
  130. package/ui/web/dist/assets/index-BdIlFieV.css +0 -1
@@ -0,0 +1,480 @@
1
+ #!/usr/bin/env python3
2
+ """Cost economics metrics for aesop.
3
+
4
+ Computes unit cost metrics derived from git stats, fleet ledger tokens, and optional pricing.
5
+
6
+ Metrics:
7
+ - cost_per_loc: total_tokens / lines_of_code (tokens per line of code written)
8
+ - cost_per_merged_pr: total_tokens / merged_prs (tokens per shipped feature)
9
+ - cost_per_wave: total_tokens / wave_count (tokens per development cycle)
10
+ - unit_economics: cost-per-backlog-item (proxy: cost per PR or commit)
11
+ - cost_estimates: optional dollar costs based on aesop.config.json pricing
12
+
13
+ Honesty caveats:
14
+ - Does NOT include Fable main-thread tokens (not in fleet ledger, only in MCP cost tracking)
15
+ - Assumes ledger reflects actual execution; missing/stale ledger = underestimated costs
16
+ - LOC from git ls-files; doesn't account for deleted branches or archived code
17
+ - Wave count from commit messages; depends on "wave-N" naming convention
18
+ - Pricing estimates require aesop.config.json with pricing map; fallback to token counts
19
+
20
+ Usage:
21
+ python cost_econ.py [--repo PATH] [--config PATH] [--json]
22
+
23
+ Returns JSON dict with economics metrics, suitable for stats.json integration.
24
+ """
25
+
26
+ import argparse
27
+ import json
28
+ import re
29
+ import subprocess
30
+ import sys
31
+ from pathlib import Path
32
+ from typing import Optional, Dict, Any
33
+
34
+ # Try to import config (may not exist in all environments)
35
+ try:
36
+ import config
37
+ except ImportError:
38
+ config = None
39
+
40
+
41
+ def _run_git(repo_root: str, *args) -> str:
42
+ """Run git command in repo, return stdout."""
43
+ try:
44
+ result = subprocess.run(
45
+ ["git"] + list(args),
46
+ cwd=str(repo_root),
47
+ capture_output=True,
48
+ text=True,
49
+ encoding="utf-8",
50
+ errors="replace",
51
+ check=False,
52
+ )
53
+ return (result.stdout or "").strip()
54
+ except FileNotFoundError:
55
+ return ""
56
+
57
+
58
+ def _get_lines_of_code(repo_root: str) -> int:
59
+ """Count total lines in tracked files (reuse from self_stats)."""
60
+ try:
61
+ output = _run_git(repo_root, "ls-files")
62
+ if not output:
63
+ return 0
64
+
65
+ files = [f.strip() for f in output.split("\n") if f.strip()]
66
+ total_lines = 0
67
+
68
+ for file_path in files:
69
+ try:
70
+ file_full_path = Path(repo_root) / file_path
71
+ if file_full_path.is_file():
72
+ with open(file_full_path, 'r', encoding='utf-8', errors='ignore') as f:
73
+ total_lines += sum(1 for _ in f)
74
+ except Exception:
75
+ continue
76
+
77
+ return total_lines
78
+ except Exception:
79
+ return 0
80
+
81
+
82
+ def _get_merged_prs(repo_root: str) -> int:
83
+ """Count merge commits with 'Merge pull request #' in message."""
84
+ try:
85
+ output = _run_git(repo_root, "log", "--format=%B")
86
+ if not output:
87
+ return 0
88
+
89
+ count = output.count("Merge pull request #")
90
+ return count
91
+ except Exception:
92
+ return 0
93
+
94
+
95
+ def _get_wave_count(repo_root: str) -> int:
96
+ """Count distinct waves from commit messages."""
97
+ try:
98
+ output = _run_git(repo_root, "log", "--format=%B")
99
+ if output:
100
+ waves = set()
101
+ for match in re.finditer(r"wave[_-]?(\d+)", output, re.IGNORECASE):
102
+ waves.add(int(match.group(1)))
103
+ if waves:
104
+ return len(waves)
105
+
106
+ # Fallback: count release tags (v*)
107
+ tags = _run_git(repo_root, "tag", "-l", "v*")
108
+ tag_count = len([t for t in tags.split("\n") if t.strip()])
109
+ return tag_count
110
+ except Exception:
111
+ return 0
112
+
113
+
114
+ def _get_total_tokens(state_dir: Path) -> int:
115
+ """Parse ledger and return total tokens (input + output)."""
116
+ ledger_file = state_dir / "ledger" / "OUTCOMES-LEDGER.md"
117
+
118
+ if not ledger_file.exists():
119
+ return 0
120
+
121
+ try:
122
+ content = ledger_file.read_text(encoding='utf-8')
123
+ except Exception:
124
+ return 0
125
+
126
+ total_tokens = 0
127
+ lines = content.strip().split('\n')
128
+
129
+ for line in lines:
130
+ line = line.strip()
131
+
132
+ if not line or all(c in '|- ' for c in line):
133
+ continue
134
+
135
+ if not line.startswith('|') or not line.endswith('|'):
136
+ continue
137
+
138
+ parts = [p.strip() for p in line.split('|')]
139
+
140
+ if len(parts) < 9:
141
+ continue
142
+
143
+ try:
144
+ tokens_in_str = parts[5]
145
+ tokens_out_str = parts[6]
146
+
147
+ if 'token' in tokens_in_str.lower(): # Skip header
148
+ continue
149
+
150
+ tokens_in = int(tokens_in_str)
151
+ tokens_out = int(tokens_out_str)
152
+
153
+ total_tokens += tokens_in + tokens_out
154
+ except (ValueError, IndexError):
155
+ continue
156
+
157
+ return total_tokens
158
+
159
+
160
+ def _load_pricing_config(config_file: Optional[str] = None) -> Optional[Dict]:
161
+ """Load pricing map from aesop.config.json.
162
+
163
+ Returns:
164
+ dict or None: pricing map {model: {input_per_mtok: float, output_per_mtok: float}}
165
+ """
166
+ if config_file:
167
+ config_path = Path(config_file)
168
+ elif config:
169
+ config_path = config.CONFIG_FILE
170
+ else:
171
+ # Try to find aesop.config.json in current directory or parent
172
+ config_path = Path("aesop.config.json")
173
+ if not config_path.exists():
174
+ config_path = Path.home() / "aesop" / "aesop.config.json"
175
+
176
+ if not config_path.exists():
177
+ return None
178
+
179
+ try:
180
+ with open(config_path, encoding='utf-8') as f:
181
+ config_data = json.load(f)
182
+ except Exception:
183
+ return None
184
+
185
+ return config_data.get("pricing", None)
186
+
187
+
188
+ def _get_total_tokens_by_model(state_dir: Path) -> Dict[str, Dict[str, int]]:
189
+ """Parse ledger and return tokens per model.
190
+
191
+ Returns:
192
+ dict: {model: {tokens_in: int, tokens_out: int, count: int}}
193
+ """
194
+ ledger_file = state_dir / "ledger" / "OUTCOMES-LEDGER.md"
195
+
196
+ if not ledger_file.exists():
197
+ return {}
198
+
199
+ try:
200
+ content = ledger_file.read_text(encoding='utf-8')
201
+ except Exception:
202
+ return {}
203
+
204
+ model_tokens = {}
205
+ lines = content.strip().split('\n')
206
+
207
+ for line in lines:
208
+ line = line.strip()
209
+
210
+ if not line or all(c in '|- ' for c in line):
211
+ continue
212
+
213
+ if not line.startswith('|') or not line.endswith('|'):
214
+ continue
215
+
216
+ parts = [p.strip() for p in line.split('|')]
217
+
218
+ if len(parts) < 9:
219
+ continue
220
+
221
+ try:
222
+ model = parts[3]
223
+ tokens_in_str = parts[5]
224
+ tokens_out_str = parts[6]
225
+
226
+ if 'model' in model.lower() or 'token' in tokens_in_str.lower(): # Skip header
227
+ continue
228
+
229
+ tokens_in = int(tokens_in_str)
230
+ tokens_out = int(tokens_out_str)
231
+
232
+ if model not in model_tokens:
233
+ model_tokens[model] = {"tokens_in": 0, "tokens_out": 0, "count": 0}
234
+
235
+ model_tokens[model]["tokens_in"] += tokens_in
236
+ model_tokens[model]["tokens_out"] += tokens_out
237
+ model_tokens[model]["count"] += 1
238
+ except (ValueError, IndexError):
239
+ continue
240
+
241
+ return model_tokens
242
+
243
+
244
+ def calculate_economics(
245
+ repo_root: str = ".",
246
+ state_dir: Optional[str] = None,
247
+ config_file: Optional[str] = None
248
+ ) -> Dict[str, Any]:
249
+ """Calculate cost economics metrics for a repository.
250
+
251
+ Args:
252
+ repo_root: Path to git repository
253
+ state_dir: Path to aesop state directory (defaults to repo_root/state)
254
+ config_file: Path to aesop.config.json for pricing (optional)
255
+
256
+ Returns:
257
+ dict: Economics metrics including:
258
+ - cost_per_loc: cost per line of code
259
+ - cost_per_merged_pr: cost per shipped feature
260
+ - cost_per_wave: cost per development cycle
261
+ - unit_economics: unit cost breakdown
262
+ - cost_estimates: optional dollar costs
263
+ """
264
+ repo_path = Path(repo_root)
265
+ if state_dir:
266
+ state_path = Path(state_dir)
267
+ else:
268
+ state_path = repo_path / "state"
269
+
270
+ # Get git metrics
271
+ loc = _get_lines_of_code(str(repo_path))
272
+ merged_prs = _get_merged_prs(str(repo_path))
273
+ wave_count = _get_wave_count(str(repo_path))
274
+
275
+ # Get token metrics
276
+ total_tokens = _get_total_tokens(state_path)
277
+ model_tokens = _get_total_tokens_by_model(state_path)
278
+
279
+ # Initialize result
280
+ result = {
281
+ "cost_per_loc": {
282
+ "lines_of_code": loc,
283
+ "total_tokens": total_tokens,
284
+ "tokens_per_loc": 0.0 if loc == 0 else total_tokens / loc,
285
+ },
286
+ "cost_per_merged_pr": {
287
+ "merged_prs": merged_prs,
288
+ "total_tokens": total_tokens,
289
+ "tokens_per_pr": 0.0 if merged_prs == 0 else total_tokens / merged_prs,
290
+ },
291
+ "cost_per_wave": {
292
+ "wave_count": wave_count,
293
+ "total_tokens": total_tokens,
294
+ "tokens_per_wave": 0.0 if wave_count == 0 else total_tokens / wave_count,
295
+ },
296
+ "unit_economics": {
297
+ "cost_per_backlog_item": (
298
+ total_tokens / merged_prs if merged_prs > 0
299
+ else (total_tokens / wave_count if wave_count > 0 else 0.0)
300
+ ),
301
+ "cost_per_wave_item": 0.0 if wave_count == 0 else total_tokens / wave_count,
302
+ "backlog_item_proxy": "merged_prs" if merged_prs > 0 else "waves",
303
+ "items_count": max(merged_prs, wave_count) or 1,
304
+ },
305
+ }
306
+
307
+ # Add pricing estimates if config available
308
+ pricing_map = _load_pricing_config(config_file)
309
+ if pricing_map and model_tokens:
310
+ total_cost = 0.0
311
+ model_costs = {}
312
+
313
+ for model, tokens in model_tokens.items():
314
+ if model in pricing_map:
315
+ pricing = pricing_map[model]
316
+ input_price = pricing.get("input_per_mtok", 0.0)
317
+ output_price = pricing.get("output_per_mtok", 0.0)
318
+
319
+ input_cost = (tokens["tokens_in"] * input_price) / 1_000_000
320
+ output_cost = (tokens["tokens_out"] * output_price) / 1_000_000
321
+ model_cost = input_cost + output_cost
322
+
323
+ model_costs[model] = {
324
+ "input_cost": input_cost,
325
+ "output_cost": output_cost,
326
+ "total_cost": model_cost,
327
+ }
328
+ total_cost += model_cost
329
+
330
+ result["cost_estimates"] = {
331
+ "total_cost_dollars": total_cost,
332
+ "cost_per_loc_dollars": 0.0 if loc == 0 else total_cost / loc,
333
+ "cost_per_pr_dollars": 0.0 if merged_prs == 0 else total_cost / merged_prs,
334
+ "by_model": model_costs,
335
+ }
336
+
337
+ return result
338
+
339
+
340
+ def get_metric_honesty_caveats() -> Dict[str, str]:
341
+ """Return honesty caveats about what metrics do/don't capture.
342
+
343
+ Returns:
344
+ dict: Mapping of metric name to caveat text
345
+ """
346
+ return {
347
+ "cost_per_loc": (
348
+ "Cost-per-LOC metric (tokens / lines of code) captures token spend only from "
349
+ "fleet ledger (parallel Haiku execution). It does NOT include:\n"
350
+ " - Fable orchestrator main-thread tokens (token tracking on MCP side only)\n"
351
+ " - Manual human review/decision time\n"
352
+ " - Deprecated code or deleted branches\n"
353
+ "Limitations:\n"
354
+ " - LOC counts from git ls-files (current HEAD); doesn't reflect deleted branches\n"
355
+ " - Ledger entries are only recorded on successful/failed agent runs\n"
356
+ " - Missing/stale ledger will underestimate costs"
357
+ ),
358
+ "cost_per_merged_pr": (
359
+ "Cost-per-PR metric captures spend only from shipped merges, using commit "
360
+ "message pattern 'Merge pull request #N'. It does NOT include:\n"
361
+ " - Abandoned PRs or branches\n"
362
+ " - Main-thread Fable tokens\n"
363
+ " - Spike/spike work that never merged\n"
364
+ "Limitations:\n"
365
+ " - Depends on GitHub merge message format; non-GitHub workflows may not count\n"
366
+ " - PRs with no ledger entries are still counted (incomplete cost attribution)"
367
+ ),
368
+ "cost_per_wave": (
369
+ "Cost-per-wave metric infers wave count from commit messages containing 'wave-N'. "
370
+ "It does NOT include:\n"
371
+ " - Implicit waves without naming\n"
372
+ " - Manual work outside the tracked waves\n"
373
+ "Limitations:\n"
374
+ " - Requires strict 'wave-N' naming; ad-hoc commits may create false waves\n"
375
+ " - Fallback to release tags (v*) if no wave messages found\n"
376
+ " - Wave boundaries not validated against STATE.md"
377
+ ),
378
+ "unit_economics": (
379
+ "Unit cost economics (cost per backlog item, cost per passing test) uses merged PRs "
380
+ "as a proxy for backlog items. Limitations:\n"
381
+ " - Assumes every PR == one backlog item (may overcounts parallel work)\n"
382
+ " - Does NOT count test count metrics (passing tests require separate test ledger)\n"
383
+ " - Cost proxy uses total tokens / PR count; doesn't attribute costs per test\n"
384
+ "Future: Requires tracker integration + test ledger for accurate cost-per-test"
385
+ ),
386
+ "cost_estimates": (
387
+ "Dollar cost estimates require aesop.config.json pricing map in format:\n"
388
+ " {\"pricing\": {\"model-id\": {\"input_per_mtok\": 0.80, \"output_per_mtok\": 4.0}}}\n"
389
+ "If pricing is missing or model not in map, no estimate provided.\n"
390
+ "Limitations:\n"
391
+ " - Pricing based on model type; no dynamic rate adjustments (batch, volume)\n"
392
+ " - Doesn't account for regional pricing, tax, or discounts\n"
393
+ " - Tokens counted only from successful ledger entries"
394
+ ),
395
+ }
396
+
397
+
398
+ def main():
399
+ """CLI entry point."""
400
+ parser = argparse.ArgumentParser(
401
+ description=__doc__,
402
+ formatter_class=argparse.RawDescriptionHelpFormatter,
403
+ )
404
+ parser.add_argument(
405
+ "--repo",
406
+ default=".",
407
+ help="Repository root (default: current directory)"
408
+ )
409
+ parser.add_argument(
410
+ "--state-dir",
411
+ help="Path to aesop state directory (default: repo_root/state)"
412
+ )
413
+ parser.add_argument(
414
+ "--config",
415
+ help="Path to aesop.config.json for pricing (optional)"
416
+ )
417
+ parser.add_argument(
418
+ "--json",
419
+ action="store_true",
420
+ help="Output machine-readable JSON"
421
+ )
422
+
423
+ args = parser.parse_args()
424
+
425
+ metrics = calculate_economics(
426
+ repo_root=args.repo,
427
+ state_dir=args.state_dir,
428
+ config_file=args.config
429
+ )
430
+
431
+ if args.json:
432
+ print(json.dumps(metrics, indent=2))
433
+ else:
434
+ # Human-readable output
435
+ print("\nCost Economics Metrics")
436
+ print("=" * 60)
437
+
438
+ print("\nCost per LOC:")
439
+ loc_data = metrics["cost_per_loc"]
440
+ print(f" Lines of Code: {loc_data['lines_of_code']:,}")
441
+ print(f" Total Tokens: {loc_data['total_tokens']:,}")
442
+ print(f" Tokens per LOC: {loc_data['tokens_per_loc']:.2f}")
443
+
444
+ print("\nCost per Merged PR:")
445
+ pr_data = metrics["cost_per_merged_pr"]
446
+ print(f" Merged PRs: {pr_data['merged_prs']}")
447
+ print(f" Total Tokens: {pr_data['total_tokens']:,}")
448
+ print(f" Tokens per PR: {pr_data['tokens_per_pr']:.2f}")
449
+
450
+ print("\nCost per Wave:")
451
+ wave_data = metrics["cost_per_wave"]
452
+ print(f" Wave Count: {wave_data['wave_count']}")
453
+ print(f" Total Tokens: {wave_data['total_tokens']:,}")
454
+ print(f" Tokens per Wave: {wave_data['tokens_per_wave']:.2f}")
455
+
456
+ print("\nUnit Economics:")
457
+ unit_data = metrics["unit_economics"]
458
+ print(f" Cost per Backlog Item: {unit_data['cost_per_backlog_item']:.2f} tokens")
459
+ print(f" Cost per Wave Item: {unit_data['cost_per_wave_item']:.2f} tokens")
460
+
461
+ if "cost_estimates" in metrics:
462
+ print("\nCost Estimates (USD):")
463
+ est = metrics["cost_estimates"]
464
+ print(f" Total Cost: ${est['total_cost_dollars']:.4f}")
465
+ print(f" Cost per LOC: ${est['cost_per_loc_dollars']:.6f}")
466
+ print(f" Cost per PR: ${est['cost_per_pr_dollars']:.4f}")
467
+
468
+ print("\n" + "=" * 60)
469
+ print("\nHonesty Caveats:")
470
+ caveats = get_metric_honesty_caveats()
471
+ for metric, caveat in caveats.items():
472
+ print(f"\n{metric}:")
473
+ for line in caveat.split("\n"):
474
+ print(f" {line}")
475
+
476
+ return 0
477
+
478
+
479
+ if __name__ == "__main__":
480
+ sys.exit(main() or 0)