@matt82198/aesop 0.1.0-rc.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +101 -2
- package/README.md +66 -15
- package/bin/cli.js +164 -41
- package/daemons/run-watchdog.sh +16 -4
- package/daemons/selfheal.sh +231 -0
- package/docs/ANY-REPO.md +427 -0
- package/docs/CONTRIBUTING.md +72 -0
- package/docs/DEMO.md +334 -0
- package/docs/INSTALL.md +70 -1
- package/docs/QUICKSTART.md +80 -0
- package/docs/README.md +33 -3
- package/docs/TEAM-STATE.md +540 -0
- package/driver/CLAUDE.md +148 -0
- package/driver/README.md +383 -0
- package/driver/aesop.config.example.json +80 -0
- package/driver/agent_driver.py +355 -0
- package/driver/backend_config.py +253 -0
- package/driver/claude_code_driver.py +198 -0
- package/driver/codex_driver.py +626 -0
- package/driver/openai_compatible_driver.py +249 -0
- package/driver/openai_transport.py +146 -0
- package/driver/verification_policy.py +75 -0
- package/driver/wave_bridge.py +246 -0
- package/driver/wave_loop.py +1041 -0
- package/hooks/pre-push-policy.sh +109 -28
- package/mcp/server.mjs +320 -4
- package/package.json +23 -15
- package/skills/CLAUDE.md +132 -2
- package/skills/buildsystem/SKILL.md +330 -0
- package/skills/buildsystem/wave-flat-dispatch.template.mjs +658 -0
- package/skills/fleet/SKILL.md +113 -0
- package/skills/power/SKILL.md +246 -131
- package/state_store/__init__.py +2 -1
- package/state_store/api.py +19 -4
- package/state_store/coordination.py +209 -0
- package/state_store/identity.py +51 -0
- package/state_store/store.py +185 -73
- package/templates/wave-presets/data.json +65 -0
- package/templates/wave-presets/library.json +65 -0
- package/templates/wave-presets/saas.json +64 -0
- package/tools/audit_report.py +388 -0
- package/tools/bench_runner.py +100 -3
- package/tools/ci_merge_wait.py +256 -35
- package/tools/ci_workflow_lint.py +430 -0
- package/tools/claudemd_drift.py +394 -0
- package/tools/claudemd_lint.py +359 -0
- package/tools/common.py +39 -3
- package/tools/cost_ceiling.py +63 -31
- package/tools/cost_econ.py +480 -0
- package/tools/defect_escape.py +252 -0
- package/tools/doctor.js +1 -1
- package/tools/eod_sweep.py +58 -0
- package/tools/fleet.js +260 -0
- package/tools/fleet_ledger.py +209 -7
- package/tools/git_identity_check.py +315 -0
- package/tools/health-score.js +40 -0
- package/tools/health_score.py +361 -0
- package/tools/metrics_gate.py +13 -4
- package/tools/mutation_test.py +401 -0
- package/tools/portability_check.py +206 -0
- package/tools/reconcile.py +7 -4
- package/tools/secret_scan.py +137 -50
- package/tools/self_stats.py +20 -0
- package/tools/transcript_digest.py +380 -0
- package/tools/verify_activity_filter.py +437 -0
- package/tools/verify_agent_inspector.py +2 -0
- package/tools/verify_dash.py +2 -0
- package/tools/verify_dispatch_panel.py +301 -0
- package/tools/verify_failure_drilldown.py +188 -0
- package/tools/verify_prboard.py +2 -0
- package/tools/verify_scorecards.py +281 -0
- package/tools/verify_submit_encoding.py +2 -0
- package/tools/verify_ui_trio.py +409 -0
- package/tools/verify_wave_telemetry.py +268 -0
- package/tools/wave_backlog_analyzer.py +490 -0
- package/tools/wave_ledger_hook.py +150 -0
- package/tools/wave_preflight.py +512 -0
- package/tools/wave_resume.py +215 -0
- package/tools/wave_templates.py +215 -0
- package/ui/agents.py +68 -14
- package/ui/collectors.py +0 -55
- package/ui/config.py +7 -2
- package/ui/cost.py +231 -12
- package/ui/handler.py +183 -0
- package/ui/quality_scorecard.py +232 -0
- package/ui/wave_audit_tail.py +213 -0
- package/ui/wave_dispatch.py +280 -0
- package/ui/wave_failure.py +288 -0
- package/ui/wave_gantt.py +152 -0
- package/ui/wave_reasoning_tail.py +176 -0
- package/ui/wave_telemetry.py +377 -0
- package/ui/web/dist/assets/index-BGbgw2Nh.js +9 -0
- package/ui/web/dist/assets/index-DqZLgwNg.css +1 -0
- package/ui/web/dist/index.html +2 -2
- package/bin/CLAUDE.md +0 -76
- package/daemons/CLAUDE.md +0 -36
- package/dash/CLAUDE.md +0 -32
- package/docs/archive/README.md +0 -3
- package/docs/archive/spikes/tiered-cognition/ACTIVATION.md +0 -125
- package/docs/archive/spikes/tiered-cognition/DESIGN.md +0 -287
- package/docs/archive/spikes/tiered-cognition/FINDINGS.md +0 -113
- package/docs/archive/spikes/tiered-cognition/README.md +0 -27
- package/docs/archive/spikes/tiered-cognition/aesop-cognition.example.md +0 -32
- package/docs/archive/spikes/tiered-cognition/force-model-policy.merged.mjs +0 -673
- package/docs/archive/spikes/tiered-cognition/strip-tools-hook.mjs +0 -434
- package/hooks/CLAUDE.md +0 -89
- package/mcp/CLAUDE.md +0 -213
- package/monitor/CLAUDE.md +0 -40
- package/scan/CLAUDE.md +0 -30
- package/state_store/CLAUDE.md +0 -39
- package/tools/CLAUDE.md +0 -79
- package/ui/CLAUDE.md +0 -127
- package/ui/web/dist/assets/index-0qQYnvMC.js +0 -9
- package/ui/web/dist/assets/index-BdIlFieV.css +0 -1
|
@@ -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)
|