@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,394 @@
1
+ #!/usr/bin/env python3
2
+ """CLAUDE.md semantic drift detector.
3
+
4
+ Detects semantic drift between documentation claims and disk reality:
5
+ 1. Files/commands referenced in domain CLAUDE.md that don't exist
6
+ 2. Domain dirs on disk missing from root map
7
+ 3. Root-map entries whose dirs are gone
8
+ 4. Documented CLI flags absent from --help
9
+
10
+ Deterministic checks only (no LLM). Per-domain report.
11
+ Exit: 0=clean, 1=drift found. Supports --json.
12
+ """
13
+
14
+ import argparse
15
+ import json
16
+ import re
17
+ import subprocess
18
+ import sys
19
+ from pathlib import Path
20
+ from typing import Dict, List, Set, Tuple, Optional
21
+
22
+
23
+ def extract_domain_map(root_claude_content: str) -> Dict[str, Optional[str]]:
24
+ """Extract domain entries from root CLAUDE.md.
25
+
26
+ Returns dict of domain_name -> description (or None).
27
+ Only entries ending with '/' are recognized as domains.
28
+ """
29
+ domains = {}
30
+ # Match lines like: - **domain/** — description
31
+ pattern = r"-\s+\*\*([a-zA-Z0-9_\-]+)/\*\*\s+—\s+([^\n]+)"
32
+ for match in re.finditer(pattern, root_claude_content):
33
+ domain_name = match.group(1)
34
+ description = match.group(2).strip()
35
+ domains[domain_name] = description
36
+ return domains
37
+
38
+
39
+ def extract_cli_specs(tool_name: str, tool_line: str) -> Set[str]:
40
+ """Extract CLI flag specifications from a tool index line.
41
+
42
+ Parses lines like:
43
+ `tool.py` — description; CLI: `--flag1 --flag2 <arg>` | `--flag3`
44
+
45
+ Returns set of flag names (e.g., {"--flag1", "--flag2", "--flag3"}).
46
+ """
47
+ flags = set()
48
+
49
+ # Look for CLI: ... section (may span to end of line or be cut by another section)
50
+ cli_match = re.search(r"CLI:\s*(.+?)(?:$)", tool_line)
51
+ if not cli_match:
52
+ return flags
53
+
54
+ cli_text = cli_match.group(1)
55
+
56
+ # Extract all --flag patterns (including those with arguments like <arg>)
57
+ # Matches: --flag-name, --flag, --flag <arg>, --flag <path>, etc.
58
+ # Handle pipes and backticks as delimiters
59
+ flag_pattern = r"--[a-zA-Z0-9\-]+"
60
+ for match in re.finditer(flag_pattern, cli_text):
61
+ flag = match.group(0)
62
+ flags.add(flag)
63
+
64
+ return flags
65
+
66
+
67
+ def check_domain_dirs_exist(
68
+ domains: Dict[str, Optional[str]], repo_root: Path
69
+ ) -> List[Dict[str, str]]:
70
+ """Check that all mapped domains exist as directories.
71
+
72
+ Returns findings for domains that don't exist.
73
+
74
+ Skips special runtime-only domains that are intentionally missing.
75
+ """
76
+ findings = []
77
+
78
+ # Domains that are documented but intentionally runtime-only (git-ignored)
79
+ runtime_only_domains = {"state"}
80
+
81
+ for domain_name in domains:
82
+ # Skip runtime-only domains
83
+ if domain_name in runtime_only_domains:
84
+ continue
85
+
86
+ domain_path = repo_root / domain_name
87
+ if not domain_path.is_dir():
88
+ findings.append({
89
+ "type": "missing-domain-dir",
90
+ "domain": domain_name,
91
+ "message": f"Domain '{domain_name}/' in root map but not on disk",
92
+ })
93
+ return findings
94
+
95
+
96
+ def check_root_map_complete(
97
+ mapped_domains: Dict[str, Optional[str]], repo_root: Path
98
+ ) -> List[Dict[str, str]]:
99
+ """Check for domains on disk that are missing from root map.
100
+
101
+ Ignores hidden dirs, special dirs (assets, state, .git, etc.).
102
+ """
103
+ findings = []
104
+
105
+ # Dirs to ignore (not domains)
106
+ ignore_dirs = {
107
+ "assets", "state", ".git", "node_modules", ".github",
108
+ ".pytest_cache", "__pycache__", ".venv", "venv",
109
+ "dist", "build", ".vscode", ".idea", "conductor3"
110
+ }
111
+
112
+ for item in repo_root.iterdir():
113
+ # Skip non-dirs
114
+ if not item.is_dir():
115
+ continue
116
+
117
+ # Skip ignored dirs
118
+ if item.name in ignore_dirs or item.name.startswith("."):
119
+ continue
120
+
121
+ # Check if this dir is in the mapped domains
122
+ if item.name not in mapped_domains:
123
+ findings.append({
124
+ "type": "unmapped-domain-dir",
125
+ "domain": item.name,
126
+ "message": f"Domain directory '{item.name}/' on disk but not in root map",
127
+ })
128
+
129
+ return findings
130
+
131
+
132
+ def check_file_references(
133
+ domain_name: str, domain_dir: Path, repo_root: Path
134
+ ) -> List[Dict[str, str]]:
135
+ """Check that files referenced in a domain CLAUDE.md exist.
136
+
137
+ Ignores runtime artifacts (state/*, BRIEF.md, etc.).
138
+ """
139
+ findings = []
140
+
141
+ # Runtime artifacts allowlist (matches claudemd_lint.py)
142
+ runtime_patterns = [
143
+ r"^\.\.?/state/",
144
+ r"heartbeat",
145
+ r"^BRIEF\.md$",
146
+ r"^PROPOSALS\.md$",
147
+ r"^BUILDLOG\.md$",
148
+ r"^MEMORY\.md$",
149
+ r"^STATE\.md$",
150
+ r"^CLAUDE\.md$",
151
+ r"^SKILL\.md$",
152
+ r"^OUTCOMES-LEDGER\.md$",
153
+ r"^tracker\.json$",
154
+ r"^ACTIONS\.log$",
155
+ r"^\./state/",
156
+ r"^state/",
157
+ ]
158
+
159
+ def is_runtime_artifact(ref: str) -> bool:
160
+ for pattern in runtime_patterns:
161
+ if re.search(pattern, ref, re.IGNORECASE):
162
+ return True
163
+ return False
164
+
165
+ # Extract file references from CLAUDE.md
166
+ claudemd_path = domain_dir / "CLAUDE.md"
167
+ if not claudemd_path.exists():
168
+ # No CLAUDE.md in this domain, nothing to check
169
+ return findings
170
+
171
+ try:
172
+ content = claudemd_path.read_text(encoding="utf-8")
173
+ except (IOError, UnicodeDecodeError):
174
+ # Can't read the file
175
+ return findings
176
+
177
+ # Extract path references (same regex as claudemd_lint.py)
178
+ pattern = r"(?:[`'\"])?([a-zA-Z0-9_.][a-zA-Z0-9_./\-]*\.(?:md|py|sh|mjs))(?:[`'\"])?"
179
+ for match in re.finditer(pattern, content):
180
+ ref = match.group(1)
181
+
182
+ # Filter false positives (same filters as claudemd_lint.py)
183
+ if len(ref) <= 2 or ref.startswith("*"):
184
+ continue
185
+ if ref.startswith("/") or "~/" in ref:
186
+ continue
187
+ if ref.startswith(".") and "/" in ref and not ref.startswith("./"):
188
+ continue
189
+ if "/path/to/" in ref or ref.startswith("path/to/"):
190
+ continue
191
+ if re.match(r"^[A-Z_]+/", ref):
192
+ continue
193
+ if ref.count(".") > 2 or "/" not in ref:
194
+ continue
195
+
196
+ # Skip compound references like "CLAUDE.md/STATE.md" (documentation patterns, not file refs)
197
+ # These are patterns listing multiple files separated by "/" (e.g., "FILE1.md/FILE2.md")
198
+ parts = ref.split("/")
199
+ if len(parts) > 1 and all(p.endswith((".md", ".py", ".sh", ".mjs")) for p in parts):
200
+ # This looks like a documentation pattern listing multiple files, not a path
201
+ continue
202
+
203
+ # Skip runtime artifacts
204
+ if is_runtime_artifact(ref):
205
+ continue
206
+
207
+ # Try to resolve the reference
208
+ target = domain_dir / ref
209
+ if not target.exists():
210
+ target = repo_root / ref
211
+ if not target.exists():
212
+ findings.append({
213
+ "type": "phantom-file-reference",
214
+ "domain": domain_name,
215
+ "message": f"{domain_name}/CLAUDE.md references non-existent '{ref}'",
216
+ })
217
+
218
+ return findings
219
+
220
+
221
+ def check_cli_flags(cli_specs: Dict[str, Set[str]], repo_root: Path) -> List[Dict[str, str]]:
222
+ """Check that documented CLI flags exist in tool --help.
223
+
224
+ Calls each tool with --help and verifies documented flags appear.
225
+ """
226
+ findings = []
227
+
228
+ for tool_name, flags in cli_specs.items():
229
+ if not flags:
230
+ continue
231
+
232
+ # Find the tool
233
+ tool_path = repo_root / "tools" / tool_name
234
+ if not tool_path.exists():
235
+ # Tool doesn't exist, skip CLI check (file check will catch it)
236
+ continue
237
+
238
+ # Try to run tool --help
239
+ try:
240
+ result = subprocess.run(
241
+ [str(tool_path), "--help"],
242
+ capture_output=True,
243
+ text=True,
244
+ timeout=5,
245
+ )
246
+ help_text = result.stdout + result.stderr
247
+ except (subprocess.TimeoutExpired, OSError):
248
+ # Can't run the tool, skip
249
+ continue
250
+
251
+ # Check if each documented flag appears in help
252
+ for flag in flags:
253
+ if flag not in help_text:
254
+ findings.append({
255
+ "type": "missing-cli-flag",
256
+ "domain": "tools",
257
+ "message": f"{tool_name}: documented flag '{flag}' not in --help output",
258
+ })
259
+
260
+ return findings
261
+
262
+
263
+ def extract_tool_index(tools_claudemd: str) -> Dict[str, Set[str]]:
264
+ """Extract tool names and their CLI specs from tools/CLAUDE.md.
265
+
266
+ Returns dict mapping tool_name -> set of flags.
267
+ """
268
+ cli_specs = {}
269
+
270
+ # Find the "Tool index" section
271
+ index_match = re.search(r"## Tool index.*?\n(.*?)(?=\n## |\Z)", tools_claudemd, re.DOTALL)
272
+ if not index_match:
273
+ return cli_specs
274
+
275
+ index_text = index_match.group(1)
276
+
277
+ # Match tool lines: `tool.py` — description with optional CLI:
278
+ # We're looking for lines like: - `name.py` — description; CLI: `...`
279
+ tool_pattern = r"-\s+`([a-zA-Z0-9_\-]+\.(py|sh|mjs))`\s+—\s+([^\n]+)"
280
+
281
+ for match in re.finditer(tool_pattern, index_text):
282
+ tool_name = match.group(1)
283
+ description = match.group(3)
284
+
285
+ flags = extract_cli_specs(tool_name, description)
286
+ if flags:
287
+ cli_specs[tool_name] = flags
288
+
289
+ return cli_specs
290
+
291
+
292
+ def run_drift_check(repo_root: Path) -> List[Dict[str, str]]:
293
+ """Run full drift check.
294
+
295
+ Returns list of finding dicts with 'type', 'domain', 'message'.
296
+ Exit: 0=clean, 1=drift found.
297
+ """
298
+ findings = []
299
+
300
+ # Read root CLAUDE.md
301
+ root_claude_path = repo_root / "CLAUDE.md"
302
+ if not root_claude_path.exists():
303
+ findings.append({
304
+ "type": "missing-root-claudemd",
305
+ "domain": "root",
306
+ "message": "Root CLAUDE.md not found",
307
+ })
308
+ return findings
309
+
310
+ try:
311
+ root_claude_content = root_claude_path.read_text(encoding="utf-8")
312
+ except (IOError, UnicodeDecodeError) as e:
313
+ findings.append({
314
+ "type": "root-claudemd-read-error",
315
+ "domain": "root",
316
+ "message": f"Failed to read root CLAUDE.md: {e}",
317
+ })
318
+ return findings
319
+
320
+ # Extract domain map
321
+ domains = extract_domain_map(root_claude_content)
322
+
323
+ # Check 1: domain dirs exist
324
+ findings.extend(check_domain_dirs_exist(domains, repo_root))
325
+
326
+ # Check 2: root map is complete
327
+ findings.extend(check_root_map_complete(domains, repo_root))
328
+
329
+ # Check 3: file references in each domain CLAUDE.md
330
+ for domain_name in domains:
331
+ domain_dir = repo_root / domain_name
332
+ if domain_dir.is_dir():
333
+ findings.extend(check_file_references(domain_name, domain_dir, repo_root))
334
+
335
+ # Check 4: CLI flags (tools only, for now)
336
+ tools_claudemd_path = repo_root / "tools" / "CLAUDE.md"
337
+ if tools_claudemd_path.exists():
338
+ try:
339
+ tools_claudemd_content = tools_claudemd_path.read_text(encoding="utf-8")
340
+ cli_specs = extract_tool_index(tools_claudemd_content)
341
+ findings.extend(check_cli_flags(cli_specs, repo_root))
342
+ except (IOError, UnicodeDecodeError):
343
+ pass
344
+
345
+ return findings
346
+
347
+
348
+ def main():
349
+ """Main entry point."""
350
+ parser = argparse.ArgumentParser(
351
+ description="CLAUDE.md semantic drift detector — detect drift between docs and disk"
352
+ )
353
+ parser.add_argument(
354
+ "--root",
355
+ type=Path,
356
+ default=Path.cwd(),
357
+ help="Repository root (default: cwd)",
358
+ )
359
+ parser.add_argument(
360
+ "--json",
361
+ action="store_true",
362
+ help="Output as JSON",
363
+ )
364
+
365
+ args = parser.parse_args()
366
+ repo_root = args.root.resolve()
367
+
368
+ if not repo_root.exists():
369
+ print(f"Error: repo root {repo_root} does not exist", file=sys.stderr)
370
+ sys.exit(2)
371
+
372
+ findings = run_drift_check(repo_root)
373
+
374
+ if args.json:
375
+ output = {
376
+ "findings": findings,
377
+ "count": len(findings),
378
+ "repo_root": str(repo_root),
379
+ }
380
+ print(json.dumps(output, indent=2))
381
+ else:
382
+ if findings:
383
+ for i, finding in enumerate(findings, 1):
384
+ print(
385
+ f"{i}. [{finding['type']}] {finding['domain']}: {finding['message']}"
386
+ )
387
+ else:
388
+ print("[OK] No semantic drift detected")
389
+
390
+ sys.exit(1 if findings else 0)
391
+
392
+
393
+ if __name__ == "__main__":
394
+ main()