@matt82198/aesop 0.3.2 → 0.4.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 (46) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +24 -9
  3. package/aesop.config.example.json +16 -0
  4. package/bin/cli.js +107 -34
  5. package/daemons/run-watchdog.sh +6 -2
  6. package/docs/CONFIGURE.md +31 -20
  7. package/docs/FIRST-WAVE.md +29 -9
  8. package/docs/INSTALL.md +181 -26
  9. package/docs/MICROKERNEL.md +452 -0
  10. package/docs/PORTING.md +4 -0
  11. package/docs/README.md +7 -3
  12. package/docs/THE-AESOP-HYPOTHESIS.md +156 -0
  13. package/driver/CLAUDE.md +123 -123
  14. package/driver/README.md +40 -2
  15. package/driver/adjudication_gate.py +367 -0
  16. package/driver/aesop.config.example.json +20 -4
  17. package/driver/backend_config.py +603 -12
  18. package/driver/claude_code_driver.py +9 -17
  19. package/driver/codex_driver.py +80 -25
  20. package/driver/context_pack.py +454 -0
  21. package/driver/openai_compatible_driver.py +53 -11
  22. package/driver/openai_transport.py +31 -10
  23. package/driver/orchestrator_backend.py +332 -0
  24. package/driver/orchestrator_driver.py +589 -0
  25. package/driver/proc_util.py +134 -0
  26. package/driver/wave_loop.py +801 -59
  27. package/driver/wave_scheduler.py +361 -37
  28. package/monitor/collect-signals.mjs +83 -0
  29. package/package.json +1 -1
  30. package/state_store/coordination.py +65 -5
  31. package/tools/ci_merge_wait.py +88 -42
  32. package/tools/ci_shard_runner.py +128 -0
  33. package/tools/doctor.js +124 -12
  34. package/tools/reproduce.js +11 -2
  35. package/tools/seated_shadow_adjudication.py +920 -0
  36. package/tools/self_stats.py +61 -6
  37. package/tools/shadow_adjudication.py +1024 -0
  38. package/tools/verify_test_suite_count.py +250 -0
  39. package/tools/verify_ui_trio_redaction_proof.py +292 -0
  40. package/tools/wave_manifest_lint.py +485 -0
  41. package/tools/wave_scorecard.py +430 -0
  42. package/ui/config.py +1 -1
  43. package/ui/cost.py +176 -3
  44. package/ui/web/dist/assets/{index-CP68RIh3.js → index-4rp6B_ID.js} +1 -1
  45. package/ui/web/dist/assets/{index-CNQxaiOW.css → index-B2lTtpsH.css} +1 -1
  46. package/ui/web/dist/index.html +2 -2
package/tools/doctor.js CHANGED
@@ -9,6 +9,7 @@
9
9
 
10
10
  const fs = require('fs');
11
11
  const path = require('path');
12
+ const os = require('os');
12
13
  const { spawnSync } = require('child_process');
13
14
  const net = require('net');
14
15
 
@@ -34,17 +35,19 @@ function colorFail() {
34
35
  function checkNodeVersion() {
35
36
  const version = parseInt(process.versions.node.split('.')[0], 10);
36
37
  const passed = version >= 18;
37
- const hint = passed ? '' : `Found Node.js v${process.versions.node}, need >=18`;
38
+ const hint = passed ? `v${process.versions.node}` : `Found Node.js v${process.versions.node}, need >=18`;
38
39
  return { passed, hint };
39
40
  }
40
41
 
41
- // Check Python available (python3 or python)
42
+ // Check Python available and version >= 3.10
42
43
  function checkPython() {
43
44
  // Try python3 first
44
- const result3 = spawnSync('python3', ['--version'], { stdio: 'ignore', timeout: 5000 });
45
+ let pythonBin = null;
46
+ let result3 = spawnSync('python3', ['--version'], { encoding: 'utf8', timeout: 5000 });
47
+
45
48
  if (result3.error && result3.error.code === 'ENOENT') {
46
49
  // python3 not found, try python fallback
47
- const result = spawnSync('python', ['--version'], { stdio: 'ignore', timeout: 5000 });
50
+ const result = spawnSync('python', ['--version'], { encoding: 'utf8', timeout: 5000 });
48
51
  if (result.error && result.error.code === 'ENOENT') {
49
52
  // Neither python3 nor python found
50
53
  return { passed: false, hint: 'python3 or python not found on PATH' };
@@ -53,13 +56,35 @@ function checkPython() {
53
56
  // python exists but returned non-zero exit code
54
57
  return { passed: false, hint: 'python found but returned non-zero exit code' };
55
58
  }
56
- return { passed: true, hint: '' };
57
- }
58
- if (result3.status !== 0) {
59
+ pythonBin = 'python';
60
+ } else if (result3.status !== 0) {
59
61
  // python3 exists but returned non-zero exit code
60
62
  return { passed: false, hint: 'python3 found but returned non-zero exit code' };
63
+ } else {
64
+ pythonBin = 'python3';
65
+ }
66
+
67
+ // Extract version from python output
68
+ let versionStr = '';
69
+ try {
70
+ const versionOutput = result3.stdout || result3.stderr || '';
71
+ const match = versionOutput.match(/Python\s+(\d+)\.(\d+)/i);
72
+ if (match) {
73
+ const major = parseInt(match[1], 10);
74
+ const minor = parseInt(match[2], 10);
75
+ versionStr = `${major}.${minor}`;
76
+ if (major > 3 || (major === 3 && minor >= 10)) {
77
+ return { passed: true, hint: `v${versionStr}` };
78
+ } else {
79
+ return { passed: false, hint: `Found Python v${versionStr}, need >=3.10` };
80
+ }
81
+ }
82
+ } catch (e) {
83
+ // Version extraction failed but python runs
84
+ return { passed: true, hint: 'Available (version check skipped)' };
61
85
  }
62
- return { passed: true, hint: '' };
86
+
87
+ return { passed: true, hint: 'Available' };
63
88
  }
64
89
 
65
90
  // Check git repo (.git directory exists)
@@ -78,13 +103,98 @@ function checkConfig() {
78
103
  return { passed: false, hint: 'aesop.config.json not found' };
79
104
  }
80
105
  const content = fs.readFileSync(configPath, 'utf8');
81
- JSON.parse(content);
106
+ const config = JSON.parse(content);
107
+
108
+ // Validate repos array exists and is an array
109
+ if (!('repos' in config)) {
110
+ return { passed: false, hint: 'Missing required field: repos' };
111
+ }
112
+ if (!Array.isArray(config.repos)) {
113
+ return { passed: false, hint: 'repos field must be an array' };
114
+ }
115
+
116
+ // Validate aesop_root exists on disk (if present in config)
117
+ if (config.aesop_root && !fs.existsSync(config.aesop_root)) {
118
+ return { passed: false, hint: `aesop_root does not exist: ${config.aesop_root}` };
119
+ }
120
+
121
+ // Validate repo paths exist (if specified)
122
+ for (const repo of config.repos) {
123
+ if (repo.path && !fs.existsSync(repo.path)) {
124
+ return { passed: false, hint: `Repo path does not exist: ${repo.path}` };
125
+ }
126
+ }
127
+
82
128
  return { passed: true, hint: '' };
83
129
  } catch (e) {
84
- return { passed: false, hint: `Config parse error: ${e.message}` };
130
+ return { passed: false, hint: `Config error: ${e.message}` };
85
131
  }
86
132
  }
87
133
 
134
+ // Check for placeholder repo URLs and warn
135
+ function checkRepoURLs() {
136
+ const configPath = path.join(CURRENT_DIR, 'aesop.config.json');
137
+ try {
138
+ if (!fs.existsSync(configPath)) {
139
+ // Skip this check if config doesn't exist (main checkConfig will fail)
140
+ return { passed: true, hint: '' };
141
+ }
142
+ const content = fs.readFileSync(configPath, 'utf8');
143
+ const config = JSON.parse(content);
144
+
145
+ // Check for placeholder URLs
146
+ const placeholderPattern = /https:\/\/github\.com\/user\//i;
147
+ if (config.repos && Array.isArray(config.repos)) {
148
+ for (const repo of config.repos) {
149
+ if (repo.url && placeholderPattern.test(repo.url)) {
150
+ return {
151
+ passed: false,
152
+ hint: `Placeholder URL found: ${repo.url}. Replace with actual repo URL or remove the repo entry.`
153
+ };
154
+ }
155
+ }
156
+ }
157
+
158
+ return { passed: true, hint: '' };
159
+ } catch (e) {
160
+ return { passed: true, hint: '' }; // Skip check on config parse error (main checkConfig handles it)
161
+ }
162
+ }
163
+
164
+ // Check if ~/.claude/skills/power/SKILL.md and ~/.claude/skills/buildsystem/SKILL.md exist (WARN only)
165
+ function checkSkillsFiles() {
166
+ const homeDir = os.homedir();
167
+ const powerSkill = path.join(homeDir, '.claude', 'skills', 'power', 'SKILL.md');
168
+ const buildsystemSkill = path.join(homeDir, '.claude', 'skills', 'buildsystem', 'SKILL.md');
169
+
170
+ const powerExists = fs.existsSync(powerSkill);
171
+ const buildsystemExists = fs.existsSync(buildsystemSkill);
172
+
173
+ // Skills are required but we WARN, not FAIL, to allow doctor to proceed on partial setup
174
+ if (!powerExists && !buildsystemExists) {
175
+ return {
176
+ passed: true, // WARN, not fail
177
+ hint: `⚠ Skills not found at ~/.claude/skills/. Required before running orchestrator. Copy: cp -r ~/.claude/skills/ here`
178
+ };
179
+ }
180
+
181
+ if (!powerExists) {
182
+ return {
183
+ passed: true, // WARN, not fail
184
+ hint: `⚠ power/SKILL.md not found at ${powerSkill}. Required before running orchestrator.`
185
+ };
186
+ }
187
+
188
+ if (!buildsystemExists) {
189
+ return {
190
+ passed: true, // WARN, not fail
191
+ hint: `⚠ buildsystem/SKILL.md not found at ${buildsystemSkill}. Required before running orchestrator.`
192
+ };
193
+ }
194
+
195
+ return { passed: true, hint: 'Both skills present' };
196
+ }
197
+
88
198
  // Check required directories exist
89
199
  function checkDirectories() {
90
200
  const requiredDirs = ['daemons', 'dash', 'monitor', 'tools', 'ui'];
@@ -174,9 +284,11 @@ function formatRow(label, status, hint) {
174
284
 
175
285
  const syncChecks = [
176
286
  { label: 'Node.js version ≥18', fn: checkNodeVersion },
177
- { label: 'Python (python3 or python)', fn: checkPython },
287
+ { label: 'Python version ≥3.10', fn: checkPython },
178
288
  { label: 'Git repository', fn: checkGitRepo },
179
- { label: 'aesop.config.json (valid JSON)', fn: checkConfig },
289
+ { label: 'aesop.config.json (structure & required fields)', fn: checkConfig },
290
+ { label: 'Repository URLs (no placeholders)', fn: checkRepoURLs },
291
+ { label: 'Skills files (power & buildsystem)', fn: checkSkillsFiles },
180
292
  { label: 'Required directories (daemons, dash, monitor, tools, ui)', fn: checkDirectories },
181
293
  { label: 'Git pre-push hook installed', fn: checkPrePushHook }
182
294
  ];
@@ -44,9 +44,10 @@ function colorSkip() {
44
44
 
45
45
  // Detect context: repo checkout vs installed package
46
46
  function detectContext() {
47
- // Repo checkout has .git/config and package.json with npm scripts
47
+ // Repo checkout has .git/config, package.json with npm scripts, AND package-lock.json
48
48
  const hasGitConfig = fs.existsSync(path.join(PACKAGE_ROOT, '.git', 'config'));
49
49
  const hasPackageJson = fs.existsSync(path.join(PACKAGE_ROOT, 'package.json'));
50
+ const hasPackageLock = fs.existsSync(path.join(PACKAGE_ROOT, 'package-lock.json'));
50
51
  const hasTestScripts = hasPackageJson && (() => {
51
52
  try {
52
53
  const pkg = JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, 'package.json'), 'utf8'));
@@ -57,7 +58,8 @@ function detectContext() {
57
58
  })();
58
59
 
59
60
  // Installed package is in node_modules with limited structure
60
- const isInstalled = !hasGitConfig && !hasTestScripts;
61
+ // OR repo-mode detected but package-lock.json is missing (fresh scaffold — degrade gracefully)
62
+ const isInstalled = (!hasGitConfig && !hasTestScripts) || (hasGitConfig && !hasPackageLock);
61
63
 
62
64
  return isInstalled ? 'installed' : 'repo';
63
65
  }
@@ -368,7 +370,14 @@ function runInstalledMode() {
368
370
  (async function main() {
369
371
  try {
370
372
  const context = detectContext();
373
+ const hasGitConfig = fs.existsSync(path.join(PACKAGE_ROOT, '.git', 'config'));
374
+ const hasPackageLock = fs.existsSync(path.join(PACKAGE_ROOT, 'package-lock.json'));
375
+ const isDegradedFromRepo = hasGitConfig && !hasPackageLock;
376
+
371
377
  console.log(`${COLORS.DIM}Context: ${context}${COLORS.RESET}`);
378
+ if (isDegradedFromRepo) {
379
+ console.log(`${COLORS.YELLOW}Scaffolded project detected — running installed-mode verification; full reproduce is for the aesop repo itself${COLORS.RESET}`);
380
+ }
372
381
 
373
382
  let results;
374
383
  if (context === 'repo') {