@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.
- package/CHANGELOG.md +117 -2
- package/README.md +59 -218
- package/bin/cli.js +168 -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/HOOK-INSTALL.md +15 -56
- package/docs/INSTALL.md +74 -4
- package/docs/PORTING.md +166 -0
- package/docs/README.md +33 -3
- package/docs/TEAM-STATE.md +372 -0
- package/docs/reproduce.md +33 -3
- package/driver/CLAUDE.md +150 -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 +673 -0
- package/driver/openai_compatible_driver.py +249 -0
- package/driver/openai_transport.py +179 -0
- package/driver/verification_policy.py +75 -0
- package/driver/wave_bridge.py +254 -0
- package/driver/wave_loop.py +1408 -0
- package/driver/wave_scheduler.py +890 -0
- package/hooks/pre-push-policy.sh +131 -33
- package/mcp/server.mjs +320 -4
- package/monitor/collect-signals.mjs +69 -0
- package/package.json +22 -14
- 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 +4 -2
- package/state_store/api.py +19 -4
- package/state_store/coordination.py +209 -0
- package/state_store/identity.py +51 -0
- package/state_store/projections.py +63 -0
- package/state_store/read_api.py +156 -0
- package/state_store/store.py +185 -73
- package/state_store/write_api.py +462 -0
- 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 +166 -43
- package/tools/cost_econ.py +480 -0
- package/tools/cost_projection.py +559 -0
- package/tools/crossos_drift.py +394 -0
- package/tools/defect_escape.py +252 -0
- package/tools/doctor.js +1 -1
- package/tools/eod_sweep.py +188 -26
- 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 +523 -0
- package/tools/portability_check.py +206 -0
- package/tools/proposals.mjs +47 -2
- package/tools/reconcile.py +7 -4
- package/tools/reproduce.js +405 -0
- package/tools/secret_scan.py +207 -65
- package/tools/self_stats.py +20 -0
- package/tools/stall_check.py +247 -16
- package/tools/stateapi_lint.py +325 -0
- package/tools/test_battery.py +173 -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_cost_panel.py +345 -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 +779 -0
- package/tools/wave_resume.py +215 -0
- package/tools/wave_templates.py +340 -0
- package/ui/agents.py +68 -14
- package/ui/collectors.py +81 -55
- package/ui/config.py +7 -2
- package/ui/cost.py +231 -12
- package/ui/handler.py +383 -55
- package/ui/quality_scorecard.py +232 -0
- package/ui/sse.py +3 -3
- 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 +383 -0
- package/ui/web/dist/assets/index-CNQxaiOW.css +1 -0
- package/ui/web/dist/assets/index-CP68RIh3.js +9 -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,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())
|
package/tools/proposals.mjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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 { }
|
package/tools/reconcile.py
CHANGED
|
@@ -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
|
|
64
|
-
python tools/reconcile.py --state-md STATE.md --
|
|
65
|
-
python tools/reconcile.py --state-md STATE.md --
|
|
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=
|
|
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)
|