@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,361 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Health Score tool — deterministic readiness score for primed projects.
4
+
5
+ Calculates a weighted 0-100 score based on:
6
+ - Config validity (JSON parseable, required fields) — 15 points
7
+ - Git pre-push hook installed — 15 points
8
+ - CLAUDE.md present and linted clean — 15 points
9
+ - State directory writable — 10 points
10
+ - Daemon heartbeats fresh (watchdog/monitor) — 15 points
11
+ - Git identity configured (user.name + user.email) — 15 points
12
+ - Secret-scan tool runnable — 15 points
13
+
14
+ Total: 100 points across 7 weighted checks.
15
+
16
+ Output: human-readable (score card with per-check status) or --json (structured).
17
+
18
+ Exit: 0 on success (always produces a score; no score is not a failure).
19
+ """
20
+ import argparse
21
+ import json
22
+ import os
23
+ import subprocess
24
+ import sys
25
+ from datetime import datetime, timezone
26
+ from pathlib import Path
27
+ from time import time
28
+
29
+ try:
30
+ from common import check_heartbeat_staleness
31
+ except ImportError:
32
+ from tools.common import check_heartbeat_staleness
33
+
34
+
35
+ def calculate_score(cwd=None):
36
+ """
37
+ Calculate health score for the primed project.
38
+
39
+ Args:
40
+ cwd (str): Working directory to check (default: current directory)
41
+
42
+ Returns:
43
+ dict: {
44
+ "score": int (0-100),
45
+ "checks": [
46
+ {
47
+ "name": str,
48
+ "passed": bool,
49
+ "weight": int,
50
+ "detail": str
51
+ },
52
+ ...
53
+ ]
54
+ }
55
+ """
56
+ if cwd is None:
57
+ cwd = os.getcwd()
58
+
59
+ cwd_path = Path(cwd)
60
+
61
+ checks = []
62
+
63
+ # 1. Config check (15 points)
64
+ config_check = _check_config(cwd_path)
65
+ checks.append({
66
+ "name": "config",
67
+ "weight": 15,
68
+ "passed": config_check["passed"],
69
+ "detail": config_check["detail"]
70
+ })
71
+
72
+ # 2. Pre-push hook check (15 points)
73
+ hook_check = _check_pre_push_hook(cwd_path)
74
+ checks.append({
75
+ "name": "hooks",
76
+ "weight": 15,
77
+ "passed": hook_check["passed"],
78
+ "detail": hook_check["detail"]
79
+ })
80
+
81
+ # 3. CLAUDE.md check (15 points)
82
+ claude_check = _check_claude_md(cwd_path)
83
+ checks.append({
84
+ "name": "claude",
85
+ "weight": 15,
86
+ "passed": claude_check["passed"],
87
+ "detail": claude_check["detail"]
88
+ })
89
+
90
+ # 4. State directory writable (10 points)
91
+ writable_check = _check_state_writable(cwd_path)
92
+ checks.append({
93
+ "name": "writable",
94
+ "weight": 10,
95
+ "passed": writable_check["passed"],
96
+ "detail": writable_check["detail"]
97
+ })
98
+
99
+ # 5. Heartbeats fresh (15 points)
100
+ heartbeat_check = _check_heartbeats(cwd_path)
101
+ checks.append({
102
+ "name": "heartbeat",
103
+ "weight": 15,
104
+ "passed": heartbeat_check["passed"],
105
+ "detail": heartbeat_check["detail"]
106
+ })
107
+
108
+ # 6. Git identity (15 points)
109
+ identity_check = _check_git_identity(cwd_path)
110
+ checks.append({
111
+ "name": "identity",
112
+ "weight": 15,
113
+ "passed": identity_check["passed"],
114
+ "detail": identity_check["detail"]
115
+ })
116
+
117
+ # 7. Secret-scan runnable (15 points)
118
+ secret_scan_check = _check_secret_scan_runnable(cwd_path)
119
+ checks.append({
120
+ "name": "secret-scan",
121
+ "weight": 15,
122
+ "passed": secret_scan_check["passed"],
123
+ "detail": secret_scan_check["detail"]
124
+ })
125
+
126
+ # Calculate weighted score
127
+ total_weight = sum(c["weight"] for c in checks)
128
+ earned_points = sum(c["weight"] for c in checks if c["passed"])
129
+ score = int((earned_points / total_weight) * 100) if total_weight > 0 else 0
130
+
131
+ return {
132
+ "score": score,
133
+ "checks": checks
134
+ }
135
+
136
+
137
+ def _check_config(cwd_path):
138
+ """Check if aesop.config.json exists and is valid JSON."""
139
+ config_path = cwd_path / "aesop.config.json"
140
+
141
+ if not config_path.exists():
142
+ return {"passed": False, "detail": "aesop.config.json not found"}
143
+
144
+ try:
145
+ content = config_path.read_text(encoding="utf-8")
146
+ json.loads(content)
147
+ return {"passed": True, "detail": "aesop.config.json valid"}
148
+ except json.JSONDecodeError as e:
149
+ return {"passed": False, "detail": f"Invalid JSON: {str(e)[:50]}"}
150
+ except Exception as e:
151
+ return {"passed": False, "detail": f"Config read error: {str(e)[:50]}"}
152
+
153
+
154
+ def _check_pre_push_hook(cwd_path):
155
+ """Check if .git/hooks/pre-push exists and is executable."""
156
+ hook_path = cwd_path / ".git" / "hooks" / "pre-push"
157
+
158
+ if not hook_path.exists():
159
+ return {"passed": False, "detail": "Pre-push hook not installed"}
160
+
161
+ if not os.access(hook_path, os.X_OK):
162
+ return {"passed": False, "detail": "Pre-push hook not executable"}
163
+
164
+ return {"passed": True, "detail": "Pre-push hook installed"}
165
+
166
+
167
+ def _check_claude_md(cwd_path):
168
+ """Check if CLAUDE.md exists."""
169
+ claude_path = cwd_path / "CLAUDE.md"
170
+
171
+ if not claude_path.exists():
172
+ return {"passed": False, "detail": "CLAUDE.md not found"}
173
+
174
+ content = claude_path.read_text(encoding="utf-8").strip()
175
+ if not content:
176
+ return {"passed": False, "detail": "CLAUDE.md is empty"}
177
+
178
+ return {"passed": True, "detail": "CLAUDE.md present"}
179
+
180
+
181
+ def _check_state_writable(cwd_path):
182
+ """Check if state directory is writable."""
183
+ state_dir = cwd_path / "state"
184
+
185
+ # Create state dir if it doesn't exist
186
+ try:
187
+ state_dir.mkdir(parents=True, exist_ok=True)
188
+ except Exception as e:
189
+ return {"passed": False, "detail": f"Cannot create state dir: {str(e)[:40]}"}
190
+
191
+ # Try to write a test file
192
+ test_file = state_dir / ".health-score-test"
193
+ try:
194
+ test_file.write_text("test", encoding="utf-8")
195
+ test_file.unlink()
196
+ return {"passed": True, "detail": "State directory writable"}
197
+ except Exception as e:
198
+ return {"passed": False, "detail": f"State dir not writable: {str(e)[:40]}"}
199
+
200
+
201
+ def _check_heartbeats(cwd_path):
202
+ """Check if daemon heartbeats are fresh."""
203
+ state_dir = cwd_path / "state"
204
+
205
+ # Check watchdog heartbeat (threshold 300s)
206
+ watchdog_file = state_dir / ".watchdog-heartbeat"
207
+ is_stale_watchdog, age_watchdog, info_watchdog = check_heartbeat_staleness(
208
+ watchdog_file, threshold_s=300
209
+ )
210
+
211
+ # Check monitor heartbeat (threshold 3600s)
212
+ monitor_file = state_dir / ".monitor-heartbeat"
213
+ is_stale_monitor, age_monitor, info_monitor = check_heartbeat_staleness(
214
+ monitor_file, threshold_s=3600
215
+ )
216
+
217
+ if is_stale_watchdog or is_stale_monitor:
218
+ details = []
219
+ if is_stale_watchdog:
220
+ if age_watchdog > 0:
221
+ details.append(f"watchdog stale ({age_watchdog}s)")
222
+ else:
223
+ details.append("watchdog missing")
224
+ if is_stale_monitor:
225
+ if age_monitor > 0:
226
+ details.append(f"monitor stale ({age_monitor}s)")
227
+ else:
228
+ details.append("monitor missing")
229
+ return {"passed": False, "detail": "; ".join(details)}
230
+
231
+ return {"passed": True, "detail": "Heartbeats fresh"}
232
+
233
+
234
+ def _check_git_identity(cwd_path):
235
+ """Check if git user.name and user.email are configured."""
236
+ try:
237
+ # Check local git config in the repo
238
+ name_result = subprocess.run(
239
+ ["git", "config", "user.name"],
240
+ cwd=str(cwd_path),
241
+ capture_output=True,
242
+ text=True,
243
+ timeout=5
244
+ )
245
+ email_result = subprocess.run(
246
+ ["git", "config", "user.email"],
247
+ cwd=str(cwd_path),
248
+ capture_output=True,
249
+ text=True,
250
+ timeout=5
251
+ )
252
+
253
+ name = name_result.stdout.strip()
254
+ email = email_result.stdout.strip()
255
+
256
+ if not name or not email:
257
+ missing = []
258
+ if not name:
259
+ missing.append("user.name")
260
+ if not email:
261
+ missing.append("user.email")
262
+ return {"passed": False, "detail": f"Git {', '.join(missing)} not set"}
263
+
264
+ return {"passed": True, "detail": "Git identity configured"}
265
+ except Exception as e:
266
+ return {"passed": False, "detail": f"Identity check error: {str(e)[:40]}"}
267
+
268
+
269
+ def _check_secret_scan_runnable(cwd_path):
270
+ """Check if secret_scan.py is present and runnable."""
271
+ secret_scan = cwd_path / "tools" / "secret_scan.py"
272
+
273
+ if not secret_scan.exists():
274
+ return {"passed": False, "detail": "secret_scan.py not found"}
275
+
276
+ # Try to run with --help
277
+ try:
278
+ result = subprocess.run(
279
+ [sys.executable, str(secret_scan), "--help"],
280
+ capture_output=True,
281
+ text=True,
282
+ timeout=5
283
+ )
284
+ if result.returncode == 0 or "usage" in result.stdout.lower():
285
+ return {"passed": True, "detail": "secret-scan runnable"}
286
+ else:
287
+ return {"passed": False, "detail": "secret-scan not responding"}
288
+ except subprocess.TimeoutExpired:
289
+ return {"passed": False, "detail": "secret-scan timeout"}
290
+ except Exception as e:
291
+ return {"passed": False, "detail": f"secret-scan error: {str(e)[:40]}"}
292
+
293
+
294
+ def format_human(result):
295
+ """Format score result as human-readable output."""
296
+ lines = []
297
+ score = result.get("score", 0)
298
+ checks = result.get("checks", [])
299
+
300
+ lines.append(f"\nHealth Score: {score}/100\n")
301
+
302
+ # Print check results
303
+ for check in checks:
304
+ status = "[PASS]" if check["passed"] else "[FAIL]"
305
+ name = check["name"].replace("-", " ").title()
306
+ detail = check.get("detail", "")
307
+ weight = check.get("weight", 0)
308
+
309
+ line = f" {status:8} {name:15} (weight: {weight:2})"
310
+ if detail:
311
+ line += f" — {detail}"
312
+ lines.append(line)
313
+
314
+ # Summary
315
+ passed = sum(1 for c in checks if c["passed"])
316
+ total = len(checks)
317
+ lines.append(f"\nSummary: {passed}/{total} checks passed")
318
+
319
+ return "\n".join(lines)
320
+
321
+
322
+ def format_json(result):
323
+ """Format score result as JSON."""
324
+ return json.dumps(result, indent=2)
325
+
326
+
327
+ def main():
328
+ """CLI entry point."""
329
+ parser = argparse.ArgumentParser(
330
+ description="Aesop health score — readiness assessment for primed projects"
331
+ )
332
+ parser.add_argument(
333
+ "--json",
334
+ action="store_true",
335
+ help="Output as JSON"
336
+ )
337
+ parser.add_argument(
338
+ "--cwd",
339
+ default=None,
340
+ help="Working directory to check (default: current directory)"
341
+ )
342
+
343
+ args = parser.parse_args()
344
+
345
+ try:
346
+ result = calculate_score(cwd=args.cwd)
347
+
348
+ if args.json:
349
+ output = format_json(result)
350
+ else:
351
+ output = format_human(result)
352
+
353
+ print(output)
354
+ sys.exit(0)
355
+ except Exception as e:
356
+ print(f"ERROR: {e}", file=sys.stderr)
357
+ sys.exit(1)
358
+
359
+
360
+ if __name__ == "__main__":
361
+ main()
@@ -75,9 +75,16 @@ class MetricsGate:
75
75
  text=True,
76
76
  check=False,
77
77
  )
78
- if result.returncode not in (0, 128): # 128 = invalid range
79
- # Fall back to empty if range is invalid
78
+ if result.returncode == 0:
79
+ # Success, continue to parse
80
+ pass
81
+ elif result.returncode == 128:
82
+ # Invalid range (ref doesn't exist) - no diff available
80
83
  return []
84
+ else:
85
+ # Other git error - fail closed (cannot verify => deny)
86
+ print(f"ERROR: git diff failed with exit code {result.returncode}", file=sys.stderr)
87
+ sys.exit(1)
81
88
 
82
89
  diff_output = result.stdout
83
90
  added_lines = []
@@ -96,8 +103,10 @@ class MetricsGate:
96
103
  added_lines.append((current_file, content))
97
104
 
98
105
  return added_lines
99
- except Exception:
100
- return []
106
+ except Exception as e:
107
+ # Unexpected error (file I/O, parsing) - fail closed (cannot verify => deny)
108
+ print(f"ERROR: git diff operation failed: {e}", file=sys.stderr)
109
+ sys.exit(1)
101
110
 
102
111
  def is_version_number(self, text: str) -> bool:
103
112
  """Check if text looks like a version number."""