@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,206 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Portability gate: scan shipped surface for hardcoded personal/environment paths.
4
+
5
+ Detects absolute Windows user paths (C:\\Users\\<name> / C:/Users/<name>),
6
+ POSIX home paths (/home/<name>, /Users/<name>), and private-machine tokens
7
+ ('conductor3', 'matt8'). Allows clearly-marked examples/defaults (lines containing
8
+ 'example', 'default', or 'e.g.').
9
+
10
+ Exit 0 clean, 1 with numbered file:line findings.
11
+ Supports --json output and --root for base directory.
12
+ """
13
+
14
+ import sys
15
+ import os
16
+ import json
17
+ import re
18
+ import glob
19
+ import argparse
20
+ from pathlib import Path
21
+
22
+
23
+ def read_package_json(root):
24
+ """Read package.json and extract 'files' array."""
25
+ pkg_path = os.path.join(root, 'package.json')
26
+ try:
27
+ with open(pkg_path, 'r') as f:
28
+ content = json.load(f)
29
+ return content.get('files', [])
30
+ except (FileNotFoundError, json.JSONDecodeError):
31
+ return []
32
+
33
+
34
+ def expand_globs(root, patterns):
35
+ """Expand glob patterns from package.json 'files' array."""
36
+ files = set()
37
+ for pattern in patterns:
38
+ # Normalize pattern to use forward slashes for glob
39
+ pattern = pattern.replace('\\', '/')
40
+ full_pattern = os.path.join(root, pattern).replace('\\', '/')
41
+
42
+ matches = glob.glob(full_pattern, recursive=True)
43
+ for match in matches:
44
+ # Use Path to normalize, convert back to string
45
+ normalized = str(Path(match))
46
+ files.add(normalized)
47
+
48
+ return sorted(files)
49
+
50
+
51
+ def is_text_file(filepath):
52
+ """Check if file is likely text (not binary)."""
53
+ binary_extensions = {
54
+ '.bin', '.exe', '.dll', '.so', '.dylib',
55
+ '.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp',
56
+ '.pdf', '.zip', '.tar', '.gz', '.rar',
57
+ '.woff', '.woff2', '.ttf', '.eot',
58
+ '.mp3', '.mp4', '.wav', '.mov'
59
+ }
60
+ _, ext = os.path.splitext(filepath.lower())
61
+ return ext not in binary_extensions
62
+
63
+
64
+ def read_file_lines(filepath):
65
+ """Read file lines, handling encoding issues gracefully."""
66
+ try:
67
+ with open(filepath, 'r', encoding='utf-8') as f:
68
+ return f.readlines()
69
+ except UnicodeDecodeError:
70
+ try:
71
+ with open(filepath, 'r', encoding='latin-1') as f:
72
+ return f.readlines()
73
+ except Exception:
74
+ return []
75
+ except Exception:
76
+ return []
77
+
78
+
79
+ def is_exception_line(line):
80
+ """Check if line is marked as example/default."""
81
+ line_lower = line.lower()
82
+ return any(marker in line_lower for marker in ['example', 'default', 'e.g.'])
83
+
84
+
85
+ def scan_line_for_paths(line):
86
+ """Scan a line for problematic paths and tokens."""
87
+ findings = []
88
+
89
+ # Windows absolute paths: C:\Users\<name> or C:/Users/<name>
90
+ windows_user_patterns = [
91
+ r'C:\\Users\\[a-zA-Z0-9_\-\.]+',
92
+ r'C:/Users/[a-zA-Z0-9_\-\.]+'
93
+ ]
94
+ for pattern in windows_user_patterns:
95
+ matches = re.finditer(pattern, line)
96
+ for match in matches:
97
+ findings.append({
98
+ 'type': 'windows_user_path',
99
+ 'path': match.group(0)
100
+ })
101
+
102
+ # POSIX home paths: /home/<name> or /Users/<name>
103
+ posix_patterns = [
104
+ r'/home/[a-zA-Z0-9_\-\.]+',
105
+ r'/Users/[a-zA-Z0-9_\-\.]+'
106
+ ]
107
+ for pattern in posix_patterns:
108
+ matches = re.finditer(pattern, line)
109
+ for match in matches:
110
+ findings.append({
111
+ 'type': 'posix_home_path',
112
+ 'path': match.group(0)
113
+ })
114
+
115
+ # Private machine tokens: 'conductor3' and 'matt8'
116
+ # These are simple word boundary matches (whole word)
117
+ for token in ['conductor3', 'matt8']:
118
+ # Use word boundaries to avoid false positives in longer identifiers
119
+ pattern = r'\b' + re.escape(token) + r'\b'
120
+ matches = re.finditer(pattern, line)
121
+ for match in matches:
122
+ findings.append({
123
+ 'type': 'private_token',
124
+ 'token': token
125
+ })
126
+
127
+ return findings
128
+
129
+
130
+ def scan_shipped_surface(root, json_output=False):
131
+ """Scan shipped surface for portability issues."""
132
+ patterns = read_package_json(root)
133
+ if not patterns:
134
+ print("Warning: Could not read package.json 'files' array", file=sys.stderr)
135
+ return []
136
+
137
+ files = expand_globs(root, patterns)
138
+ all_findings = []
139
+
140
+ for filepath in files:
141
+ if not os.path.isfile(filepath) or not is_text_file(filepath):
142
+ continue
143
+
144
+ lines = read_file_lines(filepath)
145
+ for line_num, line in enumerate(lines, 1):
146
+ # Skip exception lines (marked as example/default)
147
+ if is_exception_line(line):
148
+ continue
149
+
150
+ # Scan for issues
151
+ issues = scan_line_for_paths(line)
152
+ for issue in issues:
153
+ relative_path = os.path.relpath(filepath, root)
154
+ finding = {
155
+ 'file': relative_path,
156
+ 'line': line_num,
157
+ 'content': line.rstrip()[:100], # First 100 chars
158
+ **issue
159
+ }
160
+ all_findings.append(finding)
161
+
162
+ return all_findings
163
+
164
+
165
+ def main():
166
+ parser = argparse.ArgumentParser(
167
+ description='Portability gate: scan for hardcoded personal paths'
168
+ )
169
+ parser.add_argument(
170
+ '--root',
171
+ default='.',
172
+ help='Root directory to scan (default: current directory)'
173
+ )
174
+ parser.add_argument(
175
+ '--json',
176
+ action='store_true',
177
+ help='Output findings as JSON'
178
+ )
179
+
180
+ args = parser.parse_args()
181
+ root = os.path.abspath(args.root)
182
+
183
+ findings = scan_shipped_surface(root, json_output=args.json)
184
+
185
+ if args.json:
186
+ print(json.dumps(findings, indent=2))
187
+ else:
188
+ if findings:
189
+ print(f"Found {len(findings)} portability issue(s):", file=sys.stderr)
190
+ for i, finding in enumerate(findings, 1):
191
+ print(
192
+ f"{i}. {finding['file']}:{finding['line']}: "
193
+ f"{finding.get('type', 'unknown')}",
194
+ file=sys.stderr
195
+ )
196
+ if finding.get('path'):
197
+ print(f" Path: {finding['path']}", file=sys.stderr)
198
+ if finding.get('token'):
199
+ print(f" Token: {finding['token']}", file=sys.stderr)
200
+ print(f" {finding['content']}", file=sys.stderr)
201
+
202
+ return 1 if findings else 0
203
+
204
+
205
+ if __name__ == '__main__':
206
+ sys.exit(main())
@@ -19,6 +19,47 @@ import fs from 'node:fs';
19
19
  import path from 'node:path';
20
20
  import { acquireLock, releaseLock } from './lock.mjs';
21
21
 
22
+ // === Windows-portable atomic rename with EPERM/EBUSY retry ===
23
+ // On Windows, renaming over a file that another process has open throws EPERM.
24
+ // This wrapper provides bounded retry with exponential backoff and cleanup.
25
+ function atomicRename(tmpPath, targetPath) {
26
+ const maxRetries = 20;
27
+ const baseDelayMs = 50;
28
+
29
+ for (let i = 0; i < maxRetries; i++) {
30
+ try {
31
+ fs.renameSync(tmpPath, targetPath);
32
+ return true; // Success
33
+ } catch (e) {
34
+ if ((e.code === 'EPERM' || e.code === 'EACCES' || e.code === 'EBUSY') && i < maxRetries - 1) {
35
+ // Retry on Windows EPERM/EACCES/EBUSY (file held by reader)
36
+ // Exponential backoff: 50ms, 100ms, 150ms, ..., 950ms (max ~9.5s total)
37
+ const delayMs = baseDelayMs * (i + 1);
38
+ const start = Date.now();
39
+ while (Date.now() - start < delayMs) {
40
+ // Busy-wait to avoid scheduling overhead
41
+ }
42
+ } else {
43
+ // Final failure or non-retryable error; clean up .tmp file and return false
44
+ try {
45
+ fs.unlinkSync(tmpPath);
46
+ } catch {
47
+ // Cleanup failed; best effort
48
+ }
49
+ return false;
50
+ }
51
+ }
52
+ }
53
+
54
+ // Final failure after all retries; clean up and return false
55
+ try {
56
+ fs.unlinkSync(tmpPath);
57
+ } catch {
58
+ // Cleanup failed; best effort
59
+ }
60
+ return false;
61
+ }
62
+
22
63
  // === Arg parsing ===
23
64
  const args = process.argv.slice(2);
24
65
  let command = '';
@@ -193,11 +234,15 @@ function moveProposal(status) {
193
234
  return b.trim();
194
235
  }).filter(b => b).join('\n\n---\n\n');
195
236
 
196
- // ATOMIC WRITE: write to temp file, then rename (atomic on all platforms)
237
+ // ATOMIC WRITE: write to temp file, then rename with retry (Windows-safe)
197
238
  const tmpFile = proposalsFile + '.tmp';
198
239
  try {
199
240
  fs.writeFileSync(tmpFile, updatedContent.trim() ? updatedContent + '\n' : '', 'utf8');
200
- fs.renameSync(tmpFile, proposalsFile);
241
+ // Use atomicRename for Windows-safe atomic replace (handles EPERM/EBUSY retries)
242
+ if (!atomicRename(tmpFile, proposalsFile)) {
243
+ console.error(`Error: Could not write ${proposalsFile} after retry`);
244
+ process.exit(1);
245
+ }
201
246
  } catch (e) {
202
247
  // Clean up temp file if it exists
203
248
  try { fs.unlinkSync(tmpFile); } catch { }
@@ -60,9 +60,11 @@ manufacturing a false "in sync".
60
60
 
61
61
  ## Usage
62
62
 
63
- python tools/reconcile.py --state-md STATE.md --db state/events.db
64
- python tools/reconcile.py --state-md STATE.md --db state/events.db --resolve
65
- python tools/reconcile.py --state-md STATE.md --db state/events.db --json
63
+ python tools/reconcile.py --state-md STATE.md
64
+ python tools/reconcile.py --state-md STATE.md --resolve
65
+ python tools/reconcile.py --state-md STATE.md --json
66
+
67
+ (DB path defaults to state/tracker_events.db; override with --db if needed.)
66
68
 
67
69
  Exit codes: 0 = no drift (or drift cleanly resolved), 1 = drift detected and
68
70
  NOT resolved (report mode), 2 = usage/file error.
@@ -81,6 +83,7 @@ if str(ROOT) not in sys.path:
81
83
  sys.path.insert(0, str(ROOT))
82
84
 
83
85
  from state_store.store import EventStore # noqa: E402
86
+ from tools.common import get_state_db_path # noqa: E402
84
87
 
85
88
  PHASE_RE = re.compile(r"^##\s*Phase:\s*`([^`]+)`", re.MULTILINE)
86
89
 
@@ -251,7 +254,7 @@ def _format_human(report: dict) -> str:
251
254
  def main(argv=None) -> int:
252
255
  parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
253
256
  parser.add_argument("--state-md", default="STATE.md", help="Path to STATE.md (default: ./STATE.md)")
254
- parser.add_argument("--db", default="state/events.db", help="Path to state_store SQLite db")
257
+ parser.add_argument("--db", default=str(get_state_db_path()), help="Path to state_store SQLite db (default: state/tracker_events.db)")
255
258
  parser.add_argument("--resolve", action="store_true", help="Fix drift by writing the authoritative value to the non-authoritative side (opt-in; default is detect+report only)")
256
259
  parser.add_argument("--json", action="store_true", help="Emit machine-readable JSON instead of human text")
257
260
  args = parser.parse_args(argv)