@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
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
|
|
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📖
|
|
400
|
-
console.log('
|
|
401
|
-
console.log(
|
|
402
|
-
console.log(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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📖
|
|
804
|
-
console.log('
|
|
805
|
-
console.log(
|
|
806
|
-
console.log(
|
|
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('\
|
|
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.
|
|
812
|
-
console.log(
|
|
813
|
-
console.log('\
|
|
814
|
-
console.log('
|
|
815
|
-
console.log(`
|
|
816
|
-
console.log(`
|
|
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('\
|
|
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.
|
|
824
|
-
console.log(
|
|
825
|
-
console.log('\
|
|
826
|
-
console.log('
|
|
827
|
-
console.log(
|
|
828
|
-
console.log(
|
|
829
|
-
console.log('
|
|
830
|
-
console.log(
|
|
831
|
-
console.log(
|
|
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);
|
package/daemons/run-watchdog.sh
CHANGED
|
@@ -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
|
|
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
|
|
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
|