@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
package/bin/cli.js CHANGED
@@ -11,8 +11,8 @@ const helpFlag = args.includes('--help') || args.includes('-h');
11
11
  const forceFlag = args.includes('--force');
12
12
  const yesFlag = args.includes('--yes');
13
13
 
14
- // Check for runtime subcommands (doctor, watch, dash, status)
15
- const runtimeCommands = ['doctor', 'watch', 'dash', 'status'];
14
+ // Check for runtime subcommands (doctor, watch, dash, status, fleet, health-score, reproduce)
15
+ const runtimeCommands = ['doctor', 'watch', 'dash', 'status', 'fleet', 'health-score', 'reproduce'];
16
16
  const isRuntimeCommand = runtimeCommands.includes(args[0]);
17
17
 
18
18
  if (isRuntimeCommand) {
@@ -20,7 +20,10 @@ if (isRuntimeCommand) {
20
20
  'doctor': '../tools/doctor.js',
21
21
  'watch': '../tools/watch.js',
22
22
  'dash': '../tools/dash.js',
23
- 'status': '../tools/status.js'
23
+ 'status': '../tools/status.js',
24
+ 'fleet': '../tools/fleet.js',
25
+ 'health-score': '../tools/health-score.js',
26
+ 'reproduce': '../tools/reproduce.js'
24
27
  };
25
28
  // Load and run the appropriate runtime module
26
29
  // These modules run async code that sets process.exitCode and will cause Node to exit
@@ -71,6 +74,66 @@ function validatePort(port) {
71
74
  return !isNaN(p) && p > 0 && p < 65536 ? p : null;
72
75
  }
73
76
 
77
+ // Test if a port is available (returns Promise<boolean>)
78
+ function isPortAvailable(port) {
79
+ return new Promise((resolve) => {
80
+ const net = require('net');
81
+ const sock = net.createConnection({ port, host: '127.0.0.1', timeout: 500 });
82
+ let resolved = false;
83
+
84
+ const cleanup = () => {
85
+ try {
86
+ sock.destroy();
87
+ } catch (e) {
88
+ // Ignore cleanup errors
89
+ }
90
+ };
91
+
92
+ sock.on('connect', () => {
93
+ if (!resolved) {
94
+ resolved = true;
95
+ cleanup();
96
+ resolve(false); // Port is in use
97
+ }
98
+ });
99
+
100
+ sock.on('error', () => {
101
+ if (!resolved) {
102
+ resolved = true;
103
+ cleanup();
104
+ resolve(true); // Port is free
105
+ }
106
+ });
107
+
108
+ sock.on('timeout', () => {
109
+ if (!resolved) {
110
+ resolved = true;
111
+ cleanup();
112
+ resolve(true); // Assume free on timeout
113
+ }
114
+ });
115
+
116
+ // Fallback timeout
117
+ setTimeout(() => {
118
+ if (!resolved) {
119
+ resolved = true;
120
+ cleanup();
121
+ resolve(true);
122
+ }
123
+ }, 2000);
124
+ });
125
+ }
126
+
127
+ // Find next available port starting from basePort
128
+ async function findAvailablePort(basePort = 8770) {
129
+ for (let port = basePort; port < basePort + 100; port++) {
130
+ if (await isPortAvailable(port)) {
131
+ return port;
132
+ }
133
+ }
134
+ return basePort; // Fallback to basePort
135
+ }
136
+
74
137
  // Parse named flags
75
138
  function getFlag(flagName) {
76
139
  const idx = args.findIndex(arg => arg === flagName);
@@ -91,12 +154,16 @@ Usage:
91
154
  npx @matt82198/aesop watch
92
155
  npx @matt82198/aesop dash
93
156
  npx @matt82198/aesop status
157
+ npx @matt82198/aesop fleet
158
+ npx @matt82198/aesop reproduce
94
159
 
95
160
  Commands:
96
161
  doctor Preflight readiness check (Node.js, Python, git, config, dirs, hook, port)
97
162
  watch Launch the watchdog daemon (spawns daemons/run-watchdog.sh)
98
163
  dash Launch the web dashboard (spawns python3 ui/serve.py or python fallback)
99
164
  status One-shot fleet status snapshot (heartbeats, dashboard port, git branch)
165
+ fleet One-shot fleet snapshot (agents, heartbeats, tracker lanes, orchestrator status)
166
+ reproduce Offline verification suite (repo: full test suite; installed: self-checks)
100
167
  wizard Interactive onboarding (prompts for project name, repos, port)
101
168
 
102
169
  Arguments:
@@ -116,6 +183,8 @@ Examples:
116
183
  npx @matt82198/aesop watch # Launch watchdog daemon
117
184
  npx @matt82198/aesop dash # Launch web dashboard (default localhost:8770)
118
185
  npx @matt82198/aesop status # Show fleet status (heartbeats, port, git)
186
+ npx @matt82198/aesop fleet # Show fleet snapshot (JSON agents, heartbeats, tracker, orchestrator)
187
+ npx @matt82198/aesop reproduce # Run offline verification suite (full repo tests or installed checks)
119
188
  npx @matt82198/aesop # Creates ./aesop-fleet/ with template
120
189
  npx @matt82198/aesop my-fleet # Creates ./my-fleet/ with template
121
190
  npx @matt82198/aesop wizard # Interactive onboarding (60-second setup)
@@ -314,7 +383,7 @@ function substituteTemplate(templateContent, projectName, domainsStr, reposStr)
314
383
  return result;
315
384
  }
316
385
 
317
- function generateConfigJson(targetDir, templateRoot, projectName, reposStr, repoUrlsStr) {
386
+ function generateConfigJson(targetDir, templateRoot, projectName, reposStr, repoUrlsStr, port = 8770) {
318
387
  // Read example config
319
388
  const exampleConfigPath = path.join(templateRoot, 'aesop.config.example.json');
320
389
  let exampleConfig;
@@ -348,7 +417,8 @@ function generateConfigJson(targetDir, templateRoot, projectName, reposStr, repo
348
417
  dashboard: {
349
418
  refresh_seconds: 1,
350
419
  enable_jq_parsing: true,
351
- theme: 'dark'
420
+ theme: 'dark',
421
+ port: 8770
352
422
  },
353
423
  cardinal_rules: {
354
424
  subagent_model: 'haiku',
@@ -366,6 +436,8 @@ function generateConfigJson(targetDir, templateRoot, projectName, reposStr, repo
366
436
  exampleConfig.scripts_root = '~/scripts';
367
437
  exampleConfig.temp_root = '~/.aesop-temp';
368
438
  exampleConfig.fleet_root = os.homedir();
439
+ exampleConfig.dashboard = exampleConfig.dashboard || {};
440
+ exampleConfig.dashboard.port = port;
369
441
  exampleConfig._generated_note = 'This config uses portable ~ paths which expand at runtime on all platforms. Config loaders in ui/config.py (Python) and monitor/collect-signals.mjs (Node.js) automatically expand ~ paths to your home directory.';
370
442
 
371
443
  // Parse repos if provided
@@ -392,14 +464,14 @@ function generateConfigJson(targetDir, templateRoot, projectName, reposStr, repo
392
464
 
393
465
  // Print next steps and optionally run watchdog (returns a Promise)
394
466
  async function printNextStepsAndWatchdog(rl, targetDir, configPath, port) {
395
- console.log('\n🎯 Next 3 commands to get started:\n');
467
+ console.log('\n🎯 Next 2 commands to get started:\n');
396
468
  console.log(` 1. cd ${targetDir}`);
397
469
  console.log(' 2. bash daemons/run-watchdog.sh --once (one-time watchdog smoke test)');
398
470
  console.log(` 3. python3 ui/serve.py (or python as fallback; launch dashboard on localhost:${port})`);
399
- console.log('\n📖 After that, review:');
400
- console.log(' CLAUDE.md (pre-filled with your project info)');
401
- console.log(' aesop.config.json (pre-configured for your repos)');
402
- console.log(' docs/MEMORY-TEMPLATE.md (edit ~/.claude/MEMORY.md with your facts)\n');
471
+ console.log('\n📖 Optional: Set up your brain (Claude Code orchestration memory):\n');
472
+ console.log(' mkdir -p ~/.claude/memory');
473
+ console.log(` cp ${targetDir}/CLAUDE.md ~/.claude/CLAUDE.md (review and customize)`);
474
+ console.log(` cp ${targetDir}/MEMORY-SEED.md ~/.claude/MEMORY.md (then add your facts)\n`);
403
475
 
404
476
  return new Promise((resolve) => {
405
477
  rl.question('Run watchdog --once now? (y/N): ', (answer) => {
@@ -422,6 +494,37 @@ async function printNextStepsAndWatchdog(rl, targetDir, configPath, port) {
422
494
  });
423
495
  }
424
496
 
497
+ // Initialize git repo if it doesn't exist
498
+ function initializeGitRepo(targetDir, noGitFlag = false) {
499
+ if (noGitFlag) {
500
+ return false; // User explicitly requested no git
501
+ }
502
+
503
+ const gitDir = path.join(targetDir, '.git');
504
+ if (fs.existsSync(gitDir)) {
505
+ return false; // Git already initialized
506
+ }
507
+
508
+ try {
509
+ // Initialize git repo
510
+ execSync('git init -q', { cwd: targetDir, stdio: 'pipe' });
511
+ console.log('✓ Initialized git repository');
512
+
513
+ // Create initial commit
514
+ execSync('git config user.email "aesop-scaffold@local"', { cwd: targetDir, stdio: 'pipe' });
515
+ execSync('git config user.name "Aesop Scaffold"', { cwd: targetDir, stdio: 'pipe' });
516
+ execSync('git add -A', { cwd: targetDir, stdio: 'pipe' });
517
+ execSync('git commit -q -m "Initial aesop scaffold"', { cwd: targetDir, stdio: 'pipe' });
518
+ console.log('✓ Created initial git commit');
519
+ return true;
520
+ } catch (e) {
521
+ // Git operations failed, continue anyway
522
+ console.warn('⚠ Warning: Failed to initialize git repo automatically');
523
+ console.warn(' Run manually: cd ' + targetDir + ' && git init && git add -A && git commit -m "Initial commit"');
524
+ return false;
525
+ }
526
+ }
527
+
425
528
  function installPreCommitWaveguard(targetDir, templateRoot) {
426
529
  // Install the pre-commit waveguard hook to prevent commits during a wave
427
530
  // Try to locate .git directory
@@ -664,6 +767,7 @@ if (!isRuntimeCommand) {
664
767
  let finalTargetDir = targetDir;
665
768
  let configPath = path.join(targetDir, 'aesop.config.json');
666
769
  let dashboardPort = 8770;
770
+ const noGitFlag = args.includes('--no-git');
667
771
 
668
772
  let wizardRl = null;
669
773
 
@@ -704,7 +808,7 @@ if (!isRuntimeCommand) {
704
808
  }
705
809
  finalReposStr = selectedRepos;
706
810
 
707
- // Q3: Dashboard port
811
+ // Q3: Dashboard port (with availability check)
708
812
  let port = '';
709
813
  while (!port) {
710
814
  port = await promptUser(wizardRl, 'Dashboard port', '8770');
@@ -713,7 +817,21 @@ if (!isRuntimeCommand) {
713
817
  console.log(' ✗ Invalid port. Must be a number between 1 and 65535.');
714
818
  port = '';
715
819
  } else {
716
- dashboardPort = validPort;
820
+ // Check if port is available
821
+ const available = await isPortAvailable(validPort);
822
+ if (!available) {
823
+ console.log(` ⚠ Port ${validPort} is in use. Trying next available port...`);
824
+ const nextPort = await findAvailablePort(validPort);
825
+ if (nextPort !== validPort) {
826
+ console.log(` → Using port ${nextPort} instead`);
827
+ dashboardPort = nextPort;
828
+ } else {
829
+ dashboardPort = validPort;
830
+ }
831
+ port = validPort.toString(); // Exit loop
832
+ } else {
833
+ dashboardPort = validPort;
834
+ }
717
835
  }
718
836
  }
719
837
 
@@ -728,8 +846,15 @@ if (!isRuntimeCommand) {
728
846
  // Non-interactive defaults for wizard mode with --yes
729
847
  finalProjectName = 'my-fleet';
730
848
  finalReposStr = '';
731
- dashboardPort = 8770;
849
+ // Find available port
850
+ dashboardPort = await findAvailablePort(8770);
732
851
  console.log('🪄 Running onboarding wizard with defaults (--yes)');
852
+ } else if (!wizardMode && finalProjectName) {
853
+ // Headless mode: check and find available port
854
+ dashboardPort = await findAvailablePort(8770);
855
+ if (dashboardPort !== 8770) {
856
+ console.log(`ℹ Port 8770 is in use, using port ${dashboardPort} instead`);
857
+ }
733
858
  }
734
859
 
735
860
  // Copy template files
@@ -767,8 +892,8 @@ if (!isRuntimeCommand) {
767
892
  // But if it does, warn and skip
768
893
  console.warn(`⚠ Warning: aesop.config.json already exists at ${configPath}`);
769
894
  } else {
770
- // Generate aesop.config.json
771
- const config = generateConfigJson(finalTargetDir, templateRoot, finalProjectName, finalReposStr, finalRepoUrlsStr);
895
+ // Generate aesop.config.json with the determined port
896
+ const config = generateConfigJson(finalTargetDir, templateRoot, finalProjectName, finalReposStr, finalRepoUrlsStr, dashboardPort);
772
897
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
773
898
  console.log('✓ Generated aesop.config.json (configured for your repos)');
774
899
  }
@@ -782,10 +907,13 @@ if (!isRuntimeCommand) {
782
907
  console.log('✓ Copied MEMORY-SEED.md (template for your facts)');
783
908
  }
784
909
 
785
- // Install the pre-push hook
910
+ // Initialize git repo if needed (BEFORE installing hooks)
911
+ initializeGitRepo(finalTargetDir, noGitFlag);
912
+
913
+ // Install the pre-push hook (only works if .git exists)
786
914
  installPrePushHook(finalTargetDir, templateRoot);
787
915
 
788
- // Install the pre-commit waveguard hook
916
+ // Install the pre-commit waveguard hook (only works if .git exists)
789
917
  installPreCommitWaveguard(finalTargetDir, templateRoot);
790
918
 
791
919
  console.log(`\n✅ Scaffolded aesop template into "${finalTargetDir}" (${copiedCount} files)`);
@@ -800,35 +928,34 @@ if (!isRuntimeCommand) {
800
928
  console.log(` 1. cd ${finalTargetDir}`);
801
929
  console.log(' 2. bash daemons/run-watchdog.sh --once (one-time watchdog smoke test)');
802
930
  console.log(` 3. python3 ui/serve.py (or python as fallback; launch dashboard on localhost:${dashboardPort})`);
803
- console.log('\n📖 After that, review:');
804
- console.log(' CLAUDE.md (pre-filled with your project info)');
805
- console.log(' aesop.config.json (pre-configured for your repos)');
806
- console.log(' docs/MEMORY-TEMPLATE.md (edit ~/.claude/MEMORY.md with your facts)');
931
+ console.log('\n📖 Optional: Set up your brain (Claude Code orchestration memory):\n');
932
+ console.log(' mkdir -p ~/.claude/memory');
933
+ console.log(` cp ${finalTargetDir}/CLAUDE.md ~/.claude/CLAUDE.md (review and customize)`);
934
+ console.log(` cp ${finalTargetDir}/MEMORY-SEED.md ~/.claude/MEMORY.md (then add your facts)`);
807
935
  process.exit(0);
808
936
  } else if (finalProjectName) {
809
- console.log('\nHeadless scaffolding complete! Next steps:');
937
+ console.log('\n✅ Headless scaffolding complete!\n');
938
+ console.log('🎯 Next 3 commands to get started:\n');
810
939
  console.log(` 1. cd ${finalTargetDir}`);
811
- console.log(' 2. Review CLAUDE.md (pre-filled with your project info)');
812
- console.log(' 3. Review aesop.config.json (pre-configured for your repos)');
813
- console.log('\nInitialize your brain (Claude Code team memory):');
814
- console.log(' 4. mkdir -p ~/.claude/memory');
815
- console.log(` 5. cp ${finalTargetDir}/CLAUDE.md ~/.claude/CLAUDE.md (or review existing ~/.claude/CLAUDE.md)`);
816
- console.log(` 6. cp ${finalTargetDir}/MEMORY-SEED.md ~/.claude/MEMORY.md (then add your facts)`);
817
- console.log('\nRun the daemon and dashboard:');
818
- console.log(' 7. bash daemons/run-watchdog.sh --once (test run)');
819
- console.log(' 8. python3 ui/serve.py (or python as fallback; launch dashboard on localhost:8770)');
940
+ console.log(' 2. bash daemons/run-watchdog.sh --once (one-time watchdog smoke test)');
941
+ console.log(` 3. python3 ui/serve.py (or python as fallback; launch dashboard on localhost:${dashboardPort})`);
942
+ console.log('\n📖 Optional: Set up your brain (Claude Code orchestration memory):\n');
943
+ console.log(' mkdir -p ~/.claude/memory');
944
+ console.log(` cp ${finalTargetDir}/CLAUDE.md ~/.claude/CLAUDE.md (review and customize)`);
945
+ console.log(` cp ${finalTargetDir}/MEMORY-SEED.md ~/.claude/MEMORY.md (then add your facts)`);
820
946
  } else {
821
- console.log('\nConfiguration steps:');
947
+ console.log('\n✅ Scaffold complete!\n');
948
+ console.log('🎯 Next 3 commands to get started:\n');
822
949
  console.log(` 1. cd ${finalTargetDir}`);
823
- console.log(' 2. cp aesop.config.example.json aesop.config.json');
824
- console.log(' 3. Edit aesop.config.json with your configuration');
825
- console.log('\nInitialize your brain (Claude Code team memory):');
826
- console.log(' 4. mkdir -p ~/.claude/memory');
827
- console.log(` 5. cp ${finalTargetDir}/CLAUDE-TEMPLATE.md ~/.claude/CLAUDE.md (then edit domains/team info)`);
828
- console.log(` 6. cp ${finalTargetDir}/MEMORY-SEED.md ~/.claude/MEMORY.md (then add your facts)`);
829
- console.log('\nRun the daemon and dashboard:');
830
- console.log(' 7. bash daemons/run-watchdog.sh --once (test run)');
831
- console.log(' 8. python3 ui/serve.py (or python as fallback; launch dashboard on localhost:8770)');
950
+ console.log(' 2. bash daemons/run-watchdog.sh --once (one-time watchdog smoke test)');
951
+ console.log(` 3. python3 ui/serve.py (or python as fallback; launch dashboard on localhost:${dashboardPort})`);
952
+ console.log('\n📖 Configuration steps (optional):\n');
953
+ console.log(' cp aesop.config.example.json aesop.config.json (if not generated)');
954
+ console.log(' Edit aesop.config.json with your configuration');
955
+ console.log('\n📖 Optional: Set up your brain (Claude Code orchestration memory):\n');
956
+ console.log(' mkdir -p ~/.claude/memory');
957
+ console.log(` cp ${finalTargetDir}/CLAUDE-TEMPLATE.md ~/.claude/CLAUDE.md (then edit domains/team info)`);
958
+ console.log(` cp ${finalTargetDir}/MEMORY-SEED.md ~/.claude/MEMORY.md (then add your facts)`);
832
959
  }
833
960
  console.log('\nFor full documentation, see the README.md in the scaffolded directory.');
834
961
  process.exit(0);
@@ -25,8 +25,8 @@ elif command -v python >/dev/null 2>&1; then
25
25
  PYTHON_EXE="python"
26
26
  fi
27
27
 
28
- # Resolve conductor3 directory (sibling of AESOP_ROOT)
29
- CONDUCTOR_ROOT="$(dirname "$AESOP_ROOT")/conductor3"
28
+ # Resolve conductor3 directory (sibling of AESOP_ROOT); env var override for portability
29
+ CONDUCTOR_ROOT="${CONDUCTOR_ROOT:-$(dirname "$AESOP_ROOT")/conductor3}"
30
30
  MONITOR_HB_FILE="$CONDUCTOR_ROOT/monitor/.monitor-heartbeat"
31
31
  MONITOR_HB_STALE_THRESHOLD=600
32
32
 
@@ -59,7 +59,11 @@ acquire_lock() {
59
59
  # Check if both required files exist
60
60
  if [ -f "$lock_dir/timestamp" ] && [ -f "$lock_dir/pid" ] && [ -n "$lock_pid" ]; then
61
61
  # Both files present and pid is readable - use timestamp for stale check
62
- local lock_mtime=$(cat "$lock_dir/timestamp" 2>/dev/null || echo 0)
62
+ local lock_ts=$(cat "$lock_dir/timestamp" 2>/dev/null)
63
+ local lock_mtime=${lock_ts:-0}
64
+ if [ -z "$lock_ts" ]; then
65
+ echo "empty/corrupt lock timestamp — treating lock as stale" >&2
66
+ fi
63
67
  local now=$(date +%s)
64
68
  local lock_age=$((now - lock_mtime))
65
69
 
@@ -87,7 +91,11 @@ acquire_lock() {
87
91
 
88
92
  # Case 2: no pid file, but timestamp exists and is old (definitely abandoned)
89
93
  if [ $should_reclaim -eq 0 ] && [ -f "$lock_dir/timestamp" ]; then
90
- local lock_mtime=$(cat "$lock_dir/timestamp" 2>/dev/null || echo 0)
94
+ local lock_ts=$(cat "$lock_dir/timestamp" 2>/dev/null)
95
+ local lock_mtime=${lock_ts:-0}
96
+ if [ -z "$lock_ts" ]; then
97
+ echo "empty/corrupt lock timestamp — treating lock as stale" >&2
98
+ fi
91
99
  local now=$(date +%s)
92
100
  local lock_age=$((now - lock_mtime))
93
101
  if [ "$lock_age" -gt "$stale_threshold" ]; then
@@ -125,6 +133,7 @@ release_lock() {
125
133
  }
126
134
 
127
135
  # Check monitor heartbeat staleness; log to fleet-backup.log if missing or >600s old
136
+ # Gracefully skips if CONDUCTOR_ROOT doesn't exist (portability for non-conductor deployments)
128
137
  check_monitor_staleness() {
129
138
  local hb_file="$1"
130
139
  local stale_threshold="$2"
@@ -132,6 +141,9 @@ check_monitor_staleness() {
132
141
  if [ -z "$hb_file" ] || [ -z "$log_file" ]; then
133
142
  return
134
143
  fi
144
+ if [ ! -d "$(dirname "$hb_file")" ]; then
145
+ return
146
+ fi
135
147
  if [ ! -f "$hb_file" ]; then
136
148
  echo "[$(date '+%F %T')] SIGNAL: monitor heartbeat missing ($hb_file)" >> "$log_file"
137
149
  return
@@ -0,0 +1,231 @@
1
+ #!/usr/bin/env bash
2
+ # Self-healing fleet supervisor: detects stale sibling heartbeats and restarts daemons.
3
+ # Monitors run-watchdog.sh and run-monitor.sh with safe, idempotent recovery actions.
4
+ # Usage: selfheal.sh [--once]
5
+ #
6
+ # Configuration: export AESOP_ROOT=/path/to/aesop before running (defaults to script directory's parent).
7
+ # Testing: export AESOP_SELFHEAL_SKIP_RESTART=1 to skip actual daemon restarts (test-only flag).
8
+
9
+ set -u
10
+
11
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
12
+ AESOP_ROOT="${AESOP_ROOT:-$(dirname "$SCRIPT_DIR")}"
13
+ CONDUCTOR_ROOT="${CONDUCTOR_ROOT:-$(dirname "$AESOP_ROOT")/conductor3}"
14
+ MODE="${1:-daemon}"
15
+
16
+ SELFHEAL_LOCK_DIR="$AESOP_ROOT/state/.selfheal-lock"
17
+ SELFHEAL_LOG="$AESOP_ROOT/state/SELFHEAL.log"
18
+ SELFHEAL_STALE_THRESHOLD=600
19
+
20
+ WATCHDOG_HB="$AESOP_ROOT/state/.watchdog-heartbeat"
21
+ WATCHDOG_STALE_THRESHOLD=600
22
+
23
+ MONITOR_HB="$CONDUCTOR_ROOT/monitor/.monitor-heartbeat"
24
+ MONITOR_STALE_THRESHOLD=600
25
+
26
+ ts() {
27
+ date '+%Y-%m-%d %H:%M:%S'
28
+ }
29
+
30
+ log_heal() {
31
+ local msg="$1"
32
+ echo "[$(ts)] $msg" | tee -a "$SELFHEAL_LOG"
33
+ }
34
+
35
+ ensure_log_dir() {
36
+ mkdir -p "$(dirname "$SELFHEAL_LOG")" 2>/dev/null || true
37
+ }
38
+
39
+ acquire_lock() {
40
+ local lock_dir="$1"
41
+ local stale_threshold="$2"
42
+
43
+ mkdir -p "$(dirname "$lock_dir")" 2>/dev/null || true
44
+
45
+ if mkdir "$lock_dir" 2>/dev/null; then
46
+ if ! date +%s > "$lock_dir/timestamp" 2>/dev/null || ! echo $$ > "$lock_dir/pid" 2>/dev/null; then
47
+ rm -rf "$lock_dir" 2>/dev/null || true
48
+ return 1
49
+ fi
50
+ return 0
51
+ fi
52
+
53
+ if [ -d "$lock_dir" ]; then
54
+ local lock_pid=""
55
+ if [ -f "$lock_dir/pid" ]; then
56
+ lock_pid=$(cat "$lock_dir/pid" 2>/dev/null)
57
+ fi
58
+
59
+ if [ -f "$lock_dir/timestamp" ] && [ -f "$lock_dir/pid" ] && [ -n "$lock_pid" ]; then
60
+ local lock_ts=$(cat "$lock_dir/timestamp" 2>/dev/null)
61
+ local lock_mtime=${lock_ts:-0}
62
+ local now=$(date +%s)
63
+ local lock_age=$((now - lock_mtime))
64
+
65
+ if [ "$lock_age" -gt "$stale_threshold" ]; then
66
+ rm -rf "$lock_dir" 2>/dev/null || true
67
+ if mkdir "$lock_dir" 2>/dev/null; then
68
+ date +%s > "$lock_dir/timestamp" 2>/dev/null || true
69
+ echo $$ > "$lock_dir/pid" 2>/dev/null || true
70
+ return 2
71
+ fi
72
+ fi
73
+ else
74
+ local should_reclaim=0
75
+ if [ -f "$lock_dir/pid" ] && [ -n "$lock_pid" ]; then
76
+ if ! kill -0 "$lock_pid" 2>/dev/null; then
77
+ should_reclaim=1
78
+ fi
79
+ fi
80
+
81
+ if [ $should_reclaim -eq 0 ] && [ -f "$lock_dir/timestamp" ]; then
82
+ local lock_ts=$(cat "$lock_dir/timestamp" 2>/dev/null)
83
+ local lock_mtime=${lock_ts:-0}
84
+ local now=$(date +%s)
85
+ local lock_age=$((now - lock_mtime))
86
+
87
+ if [ "$lock_age" -gt "$stale_threshold" ]; then
88
+ should_reclaim=1
89
+ fi
90
+ fi
91
+
92
+ if [ $should_reclaim -eq 1 ]; then
93
+ rm -rf "$lock_dir" 2>/dev/null || true
94
+ if mkdir "$lock_dir" 2>/dev/null; then
95
+ date +%s > "$lock_dir/timestamp" 2>/dev/null || true
96
+ echo $$ > "$lock_dir/pid" 2>/dev/null || true
97
+ return 2
98
+ fi
99
+ fi
100
+ fi
101
+ fi
102
+
103
+ return 1
104
+ }
105
+
106
+ release_lock() {
107
+ local lock_dir="$1"
108
+ rm -rf "$lock_dir" 2>/dev/null || true
109
+ if [ -d "$lock_dir" ]; then
110
+ ensure_log_dir
111
+ log_heal "WARN: release_lock: failed to remove lock dir: $lock_dir (may be in-use on Windows)"
112
+ fi
113
+ }
114
+
115
+ is_heartbeat_stale() {
116
+ local hb_file="$1"
117
+ local stale_threshold="$2"
118
+
119
+ if [ ! -f "$hb_file" ]; then
120
+ return 0
121
+ fi
122
+
123
+ local hb_ts=$(cat "$hb_file" 2>/dev/null)
124
+ if [ -z "$hb_ts" ] || ! [[ "$hb_ts" =~ ^[0-9]+$ ]]; then
125
+ return 0
126
+ fi
127
+
128
+ local now=$(date +%s)
129
+ local age=$((now - hb_ts))
130
+
131
+ if [ "$age" -gt "$stale_threshold" ]; then
132
+ return 0
133
+ fi
134
+
135
+ return 1
136
+ }
137
+
138
+ restart_watchdog() {
139
+ ensure_log_dir
140
+
141
+ if is_heartbeat_stale "$WATCHDOG_HB" "$WATCHDOG_STALE_THRESHOLD"; then
142
+ if [ -z "${AESOP_SELFHEAL_SKIP_RESTART:-}" ]; then
143
+ log_heal "ACTION: run-watchdog heartbeat stale, restarting..."
144
+ if command -v bash >/dev/null 2>&1; then
145
+ bash "$AESOP_ROOT/daemons/run-watchdog.sh" >/dev/null 2>&1 &
146
+ log_heal "RESTART watchdog: spawned (PID: $!)"
147
+ else
148
+ log_heal "WARN: Cannot restart watchdog, bash not found"
149
+ fi
150
+ else
151
+ log_heal "DRY-RUN: watchdog heartbeat stale (would restart)"
152
+ fi
153
+ return 0
154
+ fi
155
+
156
+ return 1
157
+ }
158
+
159
+ restart_monitor() {
160
+ ensure_log_dir
161
+
162
+ if [ ! -d "$CONDUCTOR_ROOT" ]; then
163
+ return 1
164
+ fi
165
+
166
+ if is_heartbeat_stale "$MONITOR_HB" "$MONITOR_STALE_THRESHOLD"; then
167
+ if [ -z "${AESOP_SELFHEAL_SKIP_RESTART:-}" ]; then
168
+ log_heal "ACTION: run-monitor heartbeat stale, restarting..."
169
+ if command -v bash >/dev/null 2>&1; then
170
+ bash "$CONDUCTOR_ROOT/monitor/run-monitor.sh" >/dev/null 2>&1 &
171
+ log_heal "RESTART monitor: spawned (PID: $!)"
172
+ else
173
+ log_heal "WARN: Cannot restart monitor, bash not found"
174
+ fi
175
+ else
176
+ log_heal "DRY-RUN: monitor heartbeat stale (would restart)"
177
+ fi
178
+ return 0
179
+ fi
180
+
181
+ return 1
182
+ }
183
+
184
+ cycle_once() {
185
+ ensure_log_dir
186
+ log_heal "=== Selfheal cycle START ==="
187
+
188
+ local any_action=0
189
+
190
+ restart_watchdog
191
+ if [ $? -eq 0 ]; then
192
+ any_action=1
193
+ fi
194
+
195
+ restart_monitor
196
+ if [ $? -eq 0 ]; then
197
+ any_action=1
198
+ fi
199
+
200
+ if [ $any_action -eq 0 ]; then
201
+ log_heal "No stale heartbeats detected"
202
+ fi
203
+
204
+ log_heal "=== Selfheal cycle END ==="
205
+ }
206
+
207
+ main() {
208
+ lock_result=0
209
+ acquire_lock "$SELFHEAL_LOCK_DIR" "$SELFHEAL_STALE_THRESHOLD"
210
+ lock_result=$?
211
+
212
+ if [ $lock_result -eq 1 ]; then
213
+ exit 0
214
+ fi
215
+
216
+ trap "release_lock \"$SELFHEAL_LOCK_DIR\"; exit 0" INT TERM EXIT
217
+
218
+ if [ "$MODE" = "--once" ]; then
219
+ cycle_once
220
+ exit 0
221
+ fi
222
+
223
+ while true; do
224
+ cycle_once
225
+ sleep 60
226
+ done
227
+ }
228
+
229
+ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
230
+ main "$@"
231
+ fi