@matt82198/aesop 0.1.0-beta.5 → 0.1.0-rc.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 +146 -5
- package/LICENSE +66 -21
- package/README.md +97 -33
- package/aesop.config.example.json +6 -0
- package/bin/CLAUDE.md +12 -3
- package/bin/cli.js +211 -40
- package/daemons/CLAUDE.md +1 -1
- package/daemons/backup-fleet.sh +209 -51
- package/daemons/run-watchdog.sh +208 -62
- package/dash/dash-extra.mjs +73 -4
- package/dash/watchdog-gui.sh +41 -35
- package/docs/ARCHITECTURE.md +296 -0
- package/docs/CONCEPTS.md +254 -0
- package/docs/CONFIGURE.md +256 -0
- package/docs/FIRST-WAVE.md +276 -0
- package/docs/INSTALL.md +224 -0
- package/docs/README.md +176 -27
- package/docs/autonomous-swe.md +149 -0
- package/docs/reproduce.md +121 -0
- package/hooks/CLAUDE.md +28 -0
- package/hooks/install-waveguard.sh +68 -0
- package/hooks/pre-commit-waveguard.sh +26 -0
- package/hooks/pre-push-policy.sh +253 -15
- package/monitor/CLAUDE.md +3 -3
- package/monitor/collect-signals.mjs +133 -20
- package/package.json +15 -7
- package/scan/CLAUDE.md +30 -0
- package/skills/CLAUDE.md +1 -0
- package/state_store/ingest.py +18 -1
- package/state_store/projections.py +78 -4
- package/state_store/store.py +102 -7
- package/tools/CLAUDE.md +25 -18
- package/tools/alert_bridge.py +14 -10
- package/tools/bench_runner.py +438 -0
- package/tools/buildlog.py +4 -7
- package/tools/ci_merge_wait.py +215 -34
- package/tools/common.py +62 -0
- package/tools/cost_ceiling.py +191 -0
- package/tools/dash.js +102 -0
- package/tools/doctor.js +220 -0
- package/tools/fleet_ledger.py +189 -17
- package/tools/halt.py +172 -0
- package/tools/healthcheck.py +24 -21
- package/tools/heartbeat.py +4 -7
- package/tools/metrics_gate.py +8 -2
- package/tools/orchestrator_status.py +4 -9
- package/tools/reconcile.py +277 -0
- package/tools/rotate_logs.py +152 -62
- package/tools/scanner_selftest.py +28 -3
- package/tools/secret_scan.py +359 -90
- package/tools/self_stats.py +292 -4
- package/tools/status.js +187 -0
- package/tools/verify_agent_inspector.py +289 -0
- package/tools/verify_prboard.py +344 -0
- package/tools/watch.js +49 -0
- package/ui/CLAUDE.md +2 -0
- package/ui/agents.py +332 -33
- package/ui/api/tracker.py +15 -7
- package/ui/collectors.py +125 -32
- package/ui/config.py +22 -2
- package/ui/cost.py +98 -3
- package/ui/csrf.py +2 -1
- package/ui/handler.py +209 -21
- package/ui/sse.py +102 -16
- package/ui/wave_prs.py +230 -0
- package/ui/web/dist/assets/index-0qQYnvMC.js +9 -0
- package/ui/web/dist/assets/index-BdIlFieV.css +1 -0
- package/ui/web/dist/index.html +2 -2
- package/monitor/.signal-state.json +0 -3
- package/monitor/ACTIONS.log +0 -1
- package/monitor/BRIEF.md +0 -24
- package/monitor/SIGNALS.json +0 -54
- package/state_store/__pycache__/__init__.cpython-314.pyc +0 -0
- package/state_store/__pycache__/api.cpython-314.pyc +0 -0
- package/state_store/__pycache__/export.cpython-314.pyc +0 -0
- package/state_store/__pycache__/ingest.cpython-314.pyc +0 -0
- package/state_store/__pycache__/projections.cpython-314.pyc +0 -0
- package/state_store/__pycache__/store.cpython-314.pyc +0 -0
- package/tools/__pycache__/alert_bridge.cpython-314.pyc +0 -0
- package/tools/__pycache__/buildlog.cpython-314.pyc +0 -0
- package/tools/__pycache__/ci_merge_wait.cpython-314.pyc +0 -0
- package/tools/__pycache__/ensure_state.cpython-314.pyc +0 -0
- package/tools/__pycache__/eod_sweep.cpython-314.pyc +0 -0
- package/tools/__pycache__/fleet_ledger.cpython-314.pyc +0 -0
- package/tools/__pycache__/fleet_prompt_extractor.cpython-314.pyc +0 -0
- package/tools/__pycache__/healthcheck.cpython-314.pyc +0 -0
- package/tools/__pycache__/heartbeat.cpython-314.pyc +0 -0
- package/tools/__pycache__/inbox_drain.cpython-314.pyc +0 -0
- package/tools/__pycache__/launch_tui.cpython-314.pyc +0 -0
- package/tools/__pycache__/metrics_gate.cpython-314.pyc +0 -0
- package/tools/__pycache__/orchestrator_status.cpython-314.pyc +0 -0
- package/tools/__pycache__/power_selftest.cpython-314.pyc +0 -0
- package/tools/__pycache__/prepublish_scan.cpython-314.pyc +0 -0
- package/tools/__pycache__/rotate_logs.cpython-314.pyc +0 -0
- package/tools/__pycache__/scanner_selftest.cpython-314.pyc +0 -0
- package/tools/__pycache__/secret_scan.cpython-314.pyc +0 -0
- package/tools/__pycache__/self_stats.cpython-314.pyc +0 -0
- package/tools/__pycache__/session_usage_summary.cpython-314.pyc +0 -0
- package/tools/__pycache__/stall_check.cpython-314.pyc +0 -0
- package/tools/__pycache__/transcript_replay.cpython-314.pyc +0 -0
- package/tools/__pycache__/transcript_timeline.cpython-314.pyc +0 -0
- package/tools/__pycache__/verify_dash.cpython-314.pyc +0 -0
- package/tools/__pycache__/verify_submit_encoding.cpython-314.pyc +0 -0
- package/ui/__pycache__/agents.cpython-314.pyc +0 -0
- package/ui/__pycache__/collectors.cpython-314.pyc +0 -0
- package/ui/__pycache__/config.cpython-314.pyc +0 -0
- package/ui/__pycache__/cost.cpython-314.pyc +0 -0
- package/ui/__pycache__/csrf.cpython-314.pyc +0 -0
- package/ui/__pycache__/handler.cpython-314.pyc +0 -0
- package/ui/__pycache__/render.cpython-314.pyc +0 -0
- package/ui/__pycache__/serve.cpython-314.pyc +0 -0
- package/ui/__pycache__/sse.cpython-314.pyc +0 -0
- package/ui/api/__pycache__/__init__.cpython-314.pyc +0 -0
- package/ui/api/__pycache__/submit.cpython-314.pyc +0 -0
- package/ui/api/__pycache__/tracker.cpython-314.pyc +0 -0
- package/ui/web/.gitattributes +0 -13
- package/ui/web/dist/assets/index-2LZDQirC.js +0 -9
- package/ui/web/dist/assets/index-D4M1qyOv.css +0 -1
- package/ui/web/index.html +0 -13
- package/ui/web/package-lock.json +0 -2225
- package/ui/web/package.json +0 -26
- package/ui/web/src/App.test.tsx +0 -74
- package/ui/web/src/App.tsx +0 -142
- package/ui/web/src/CONTRIBUTING-UI.md +0 -49
- package/ui/web/src/components/AgentRow.css +0 -187
- package/ui/web/src/components/AgentRow.test.tsx +0 -209
- package/ui/web/src/components/AgentRow.tsx +0 -207
- package/ui/web/src/components/AgentsPanel.css +0 -108
- package/ui/web/src/components/AgentsPanel.test.tsx +0 -41
- package/ui/web/src/components/AgentsPanel.tsx +0 -58
- package/ui/web/src/components/AlertsPanel.css +0 -88
- package/ui/web/src/components/AlertsPanel.test.tsx +0 -51
- package/ui/web/src/components/AlertsPanel.tsx +0 -67
- package/ui/web/src/components/BacklogPanel.test.tsx +0 -126
- package/ui/web/src/components/BacklogPanel.tsx +0 -122
- package/ui/web/src/components/CostChart.css +0 -110
- package/ui/web/src/components/CostChart.test.tsx +0 -144
- package/ui/web/src/components/CostChart.tsx +0 -152
- package/ui/web/src/components/CostTable.css +0 -93
- package/ui/web/src/components/CostTable.test.tsx +0 -165
- package/ui/web/src/components/CostTable.tsx +0 -94
- package/ui/web/src/components/EventsFeed.css +0 -68
- package/ui/web/src/components/EventsFeed.test.tsx +0 -36
- package/ui/web/src/components/EventsFeed.tsx +0 -31
- package/ui/web/src/components/HealthHeader.css +0 -137
- package/ui/web/src/components/HealthHeader.test.tsx +0 -278
- package/ui/web/src/components/HealthHeader.tsx +0 -281
- package/ui/web/src/components/InboxForm.css +0 -135
- package/ui/web/src/components/InboxForm.test.tsx +0 -208
- package/ui/web/src/components/InboxForm.tsx +0 -116
- package/ui/web/src/components/MessagesTail.module.css +0 -144
- package/ui/web/src/components/MessagesTail.test.tsx +0 -176
- package/ui/web/src/components/MessagesTail.tsx +0 -94
- package/ui/web/src/components/ReposPanel.css +0 -90
- package/ui/web/src/components/ReposPanel.test.tsx +0 -45
- package/ui/web/src/components/ReposPanel.tsx +0 -67
- package/ui/web/src/components/Scorecard.css +0 -106
- package/ui/web/src/components/Scorecard.test.tsx +0 -117
- package/ui/web/src/components/Scorecard.tsx +0 -85
- package/ui/web/src/components/Timeline.module.css +0 -151
- package/ui/web/src/components/Timeline.test.tsx +0 -215
- package/ui/web/src/components/Timeline.tsx +0 -99
- package/ui/web/src/components/TrackerBoard.test.tsx +0 -121
- package/ui/web/src/components/TrackerBoard.tsx +0 -107
- package/ui/web/src/components/TrackerCard.test.tsx +0 -180
- package/ui/web/src/components/TrackerCard.tsx +0 -160
- package/ui/web/src/components/TrackerForm.test.tsx +0 -189
- package/ui/web/src/components/TrackerForm.tsx +0 -144
- package/ui/web/src/lib/api.ts +0 -218
- package/ui/web/src/lib/format.test.ts +0 -89
- package/ui/web/src/lib/format.ts +0 -103
- package/ui/web/src/lib/sanitizeUrl.test.ts +0 -84
- package/ui/web/src/lib/sanitizeUrl.ts +0 -38
- package/ui/web/src/lib/types.ts +0 -230
- package/ui/web/src/lib/useHashRoute.test.ts +0 -60
- package/ui/web/src/lib/useHashRoute.ts +0 -23
- package/ui/web/src/lib/useSSE.ts +0 -175
- package/ui/web/src/main.tsx +0 -10
- package/ui/web/src/styles/global.css +0 -179
- package/ui/web/src/styles/theme.css +0 -184
- package/ui/web/src/styles/work.css +0 -572
- package/ui/web/src/test/fixtures.ts +0 -385
- package/ui/web/src/test/setup.ts +0 -49
- package/ui/web/src/views/Activity.module.css +0 -43
- package/ui/web/src/views/Activity.test.tsx +0 -89
- package/ui/web/src/views/Activity.tsx +0 -31
- package/ui/web/src/views/Cost.css +0 -87
- package/ui/web/src/views/Cost.test.tsx +0 -142
- package/ui/web/src/views/Cost.tsx +0 -54
- package/ui/web/src/views/Overview.css +0 -51
- package/ui/web/src/views/Overview.test.tsx +0 -76
- package/ui/web/src/views/Overview.tsx +0 -46
- package/ui/web/src/views/Work.test.tsx +0 -82
- package/ui/web/src/views/Work.tsx +0 -79
- package/ui/web/src/vite-env.d.ts +0 -10
- package/ui/web/tsconfig.json +0 -22
- package/ui/web/vite.config.ts +0 -25
- package/ui/web/vitest.config.ts +0 -12
package/bin/cli.js
CHANGED
|
@@ -11,6 +11,24 @@ 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'];
|
|
16
|
+
const isRuntimeCommand = runtimeCommands.includes(args[0]);
|
|
17
|
+
|
|
18
|
+
if (isRuntimeCommand) {
|
|
19
|
+
const commandMap = {
|
|
20
|
+
'doctor': '../tools/doctor.js',
|
|
21
|
+
'watch': '../tools/watch.js',
|
|
22
|
+
'dash': '../tools/dash.js',
|
|
23
|
+
'status': '../tools/status.js'
|
|
24
|
+
};
|
|
25
|
+
// Load and run the appropriate runtime module
|
|
26
|
+
// These modules run async code that sets process.exitCode and will cause Node to exit
|
|
27
|
+
require(commandMap[args[0]]);
|
|
28
|
+
// Return immediately to prevent further CLI scaffolding logic from running
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
14
32
|
// Detect if stdin is a TTY (interactive terminal)
|
|
15
33
|
function isInteractive() {
|
|
16
34
|
return process.stdin.isTTY && process.stdout.isTTY;
|
|
@@ -69,8 +87,16 @@ aesop — Multi-agent orchestration template scaffolder
|
|
|
69
87
|
Usage:
|
|
70
88
|
npx @matt82198/aesop [target-dir] [options]
|
|
71
89
|
npx @matt82198/aesop wizard [options]
|
|
90
|
+
npx @matt82198/aesop doctor
|
|
91
|
+
npx @matt82198/aesop watch
|
|
92
|
+
npx @matt82198/aesop dash
|
|
93
|
+
npx @matt82198/aesop status
|
|
72
94
|
|
|
73
95
|
Commands:
|
|
96
|
+
doctor Preflight readiness check (Node.js, Python, git, config, dirs, hook, port)
|
|
97
|
+
watch Launch the watchdog daemon (spawns daemons/run-watchdog.sh)
|
|
98
|
+
dash Launch the web dashboard (spawns python3 ui/serve.py or python fallback)
|
|
99
|
+
status One-shot fleet status snapshot (heartbeats, dashboard port, git branch)
|
|
74
100
|
wizard Interactive onboarding (prompts for project name, repos, port)
|
|
75
101
|
|
|
76
102
|
Arguments:
|
|
@@ -83,8 +109,13 @@ Options:
|
|
|
83
109
|
--name <name> Project name (for headless scaffolding; generates CLAUDE.md + aesop.config.json)
|
|
84
110
|
--domains <list> Comma-separated domain list (e.g., "api,worker,monitoring")
|
|
85
111
|
--repos <paths> Comma-separated repo paths (e.g., "/path/to/repo1,/path/to/repo2")
|
|
112
|
+
--repo-urls <urls> Comma-separated repo URLs (e.g., "https://github.com/user/repo1.git,https://github.com/user/repo2.git")
|
|
86
113
|
|
|
87
114
|
Examples:
|
|
115
|
+
npx @matt82198/aesop doctor # Run preflight checks before starting
|
|
116
|
+
npx @matt82198/aesop watch # Launch watchdog daemon
|
|
117
|
+
npx @matt82198/aesop dash # Launch web dashboard (default localhost:8770)
|
|
118
|
+
npx @matt82198/aesop status # Show fleet status (heartbeats, port, git)
|
|
88
119
|
npx @matt82198/aesop # Creates ./aesop-fleet/ with template
|
|
89
120
|
npx @matt82198/aesop my-fleet # Creates ./my-fleet/ with template
|
|
90
121
|
npx @matt82198/aesop wizard # Interactive onboarding (60-second setup)
|
|
@@ -106,8 +137,8 @@ Interactive wizard flow:
|
|
|
106
137
|
After scaffolding, cd into the directory and:
|
|
107
138
|
1. Review CLAUDE.md (pre-filled with your project info)
|
|
108
139
|
2. Review aesop.config.json (pre-configured for your repos)
|
|
109
|
-
3. Run: bash daemons/run-watchdog.sh
|
|
110
|
-
4. Launch dashboard:
|
|
140
|
+
3. Run: aesop watch (or bash daemons/run-watchdog.sh)
|
|
141
|
+
4. Launch dashboard: aesop dash (or python3 ui/serve.py)
|
|
111
142
|
`);
|
|
112
143
|
process.exit(0);
|
|
113
144
|
}
|
|
@@ -134,9 +165,9 @@ if (wizardModeRequested && wizardArgIndex >= 0) {
|
|
|
134
165
|
}
|
|
135
166
|
|
|
136
167
|
// Extract targetDir (first non-flag argument, excluding flag values)
|
|
137
|
-
// Build set of indices consumed as flag values (tokens after --name/--domains/--repos)
|
|
168
|
+
// Build set of indices consumed as flag values (tokens after --name/--domains/--repos/--repo-urls)
|
|
138
169
|
const consumedIndices = new Set();
|
|
139
|
-
const flagsWithValues = ['--name', '--domains', '--repos'];
|
|
170
|
+
const flagsWithValues = ['--name', '--domains', '--repos', '--repo-urls'];
|
|
140
171
|
for (let i = 0; i < args.length; i++) {
|
|
141
172
|
if (flagsWithValues.includes(args[i]) && i + 1 < args.length) {
|
|
142
173
|
consumedIndices.add(i + 1);
|
|
@@ -153,12 +184,13 @@ const targetDir = args.find((arg, idx) =>
|
|
|
153
184
|
const projectName = getFlag('--name');
|
|
154
185
|
const domainsStr = getFlag('--domains');
|
|
155
186
|
const reposStr = getFlag('--repos');
|
|
187
|
+
const repoUrlsStr = getFlag('--repo-urls');
|
|
156
188
|
|
|
157
189
|
// Validate target directory doesn't exist or is empty (except for .git and aesop files)
|
|
158
190
|
if (fs.existsSync(targetDir)) {
|
|
159
191
|
const contents = fs.readdirSync(targetDir);
|
|
160
192
|
// Allow .git and aesop scaffolded files to already exist (for idempotency)
|
|
161
|
-
const aesopDirs = ['daemons', 'dash', 'monitor', 'tools', 'ui', 'docs', '.git', 'state'];
|
|
193
|
+
const aesopDirs = ['daemons', 'dash', 'monitor', 'tools', 'ui', 'docs', 'state_store', 'skills', 'mcp', 'scan', 'hooks', '.git', 'state'];
|
|
162
194
|
const aesopFiles = [
|
|
163
195
|
'aesop.config.example.json',
|
|
164
196
|
'aesop.config.json',
|
|
@@ -211,6 +243,11 @@ const filesToCopy = [
|
|
|
211
243
|
'tools',
|
|
212
244
|
'ui',
|
|
213
245
|
'docs',
|
|
246
|
+
'state_store',
|
|
247
|
+
'skills',
|
|
248
|
+
'mcp',
|
|
249
|
+
'scan',
|
|
250
|
+
'hooks',
|
|
214
251
|
'aesop.config.example.json',
|
|
215
252
|
'README.md',
|
|
216
253
|
'LICENSE',
|
|
@@ -277,7 +314,7 @@ function substituteTemplate(templateContent, projectName, domainsStr, reposStr)
|
|
|
277
314
|
return result;
|
|
278
315
|
}
|
|
279
316
|
|
|
280
|
-
function generateConfigJson(targetDir, templateRoot, projectName, reposStr) {
|
|
317
|
+
function generateConfigJson(targetDir, templateRoot, projectName, reposStr, repoUrlsStr) {
|
|
281
318
|
// Read example config
|
|
282
319
|
const exampleConfigPath = path.join(templateRoot, 'aesop.config.example.json');
|
|
283
320
|
let exampleConfig;
|
|
@@ -292,6 +329,7 @@ function generateConfigJson(targetDir, templateRoot, projectName, reposStr) {
|
|
|
292
329
|
brain_root: path.join(os.homedir(), '.claude'),
|
|
293
330
|
scripts_root: path.join(os.homedir(), 'scripts'),
|
|
294
331
|
temp_root: path.join(os.tmpdir()),
|
|
332
|
+
fleet_root: os.homedir(),
|
|
295
333
|
repos: [],
|
|
296
334
|
watchdog: {
|
|
297
335
|
cycle_seconds: 150,
|
|
@@ -327,18 +365,26 @@ function generateConfigJson(targetDir, templateRoot, projectName, reposStr) {
|
|
|
327
365
|
exampleConfig.brain_root = '~/.claude';
|
|
328
366
|
exampleConfig.scripts_root = '~/scripts';
|
|
329
367
|
exampleConfig.temp_root = '~/.aesop-temp';
|
|
368
|
+
exampleConfig.fleet_root = os.homedir();
|
|
330
369
|
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.';
|
|
331
370
|
|
|
332
371
|
// Parse repos if provided
|
|
333
372
|
if (reposStr) {
|
|
334
373
|
const repos = reposStr.split(',').map(r => r.trim()).filter(r => r);
|
|
374
|
+
const repoUrls = repoUrlsStr ? repoUrlsStr.split(',').map(u => u.trim()).filter(u => u) : [];
|
|
375
|
+
|
|
335
376
|
exampleConfig.repos = repos.map((repoPath, idx) => ({
|
|
336
377
|
path: repoPath,
|
|
337
378
|
name: path.basename(repoPath),
|
|
338
|
-
url: `https://github.com/user/${path.basename(repoPath)}.git`,
|
|
379
|
+
url: repoUrls[idx] || `https://github.com/user/${path.basename(repoPath)}.git`,
|
|
339
380
|
primary_branch: 'main',
|
|
340
381
|
backup_branch: 'backup/wip'
|
|
341
382
|
}));
|
|
383
|
+
|
|
384
|
+
// Add _repos_note if URLs weren't provided (user must edit them)
|
|
385
|
+
if (!repoUrlsStr) {
|
|
386
|
+
exampleConfig._repos_note = 'Repository URLs are currently placeholders (https://github.com/user/<name>.git). Update each repo.url in the repos array with the actual repository URL. This is a required manual step before the daemon can clone repositories.';
|
|
387
|
+
}
|
|
342
388
|
}
|
|
343
389
|
|
|
344
390
|
return exampleConfig;
|
|
@@ -349,7 +395,7 @@ async function printNextStepsAndWatchdog(rl, targetDir, configPath, port) {
|
|
|
349
395
|
console.log('\n🎯 Next 3 commands to get started:\n');
|
|
350
396
|
console.log(` 1. cd ${targetDir}`);
|
|
351
397
|
console.log(' 2. bash daemons/run-watchdog.sh --once (one-time watchdog smoke test)');
|
|
352
|
-
console.log(` 3.
|
|
398
|
+
console.log(` 3. python3 ui/serve.py (or python as fallback; launch dashboard on localhost:${port})`);
|
|
353
399
|
console.log('\n📖 After that, review:');
|
|
354
400
|
console.log(' • CLAUDE.md (pre-filled with your project info)');
|
|
355
401
|
console.log(' • aesop.config.json (pre-configured for your repos)');
|
|
@@ -376,6 +422,133 @@ async function printNextStepsAndWatchdog(rl, targetDir, configPath, port) {
|
|
|
376
422
|
});
|
|
377
423
|
}
|
|
378
424
|
|
|
425
|
+
function installPreCommitWaveguard(targetDir, templateRoot) {
|
|
426
|
+
// Install the pre-commit waveguard hook to prevent commits during a wave
|
|
427
|
+
// Try to locate .git directory
|
|
428
|
+
const gitDir = path.join(targetDir, '.git');
|
|
429
|
+
if (!fs.existsSync(gitDir)) {
|
|
430
|
+
// No git repo, skip hook installation silently
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// SECURITY: Check if .git itself is a symlink/junction (refuse to follow it outside targetDir)
|
|
435
|
+
try {
|
|
436
|
+
const gitDirLstat = fs.lstatSync(gitDir);
|
|
437
|
+
if (gitDirLstat.isSymbolicLink()) {
|
|
438
|
+
console.warn('⚠ Warning: .git is a symlink (security risk)');
|
|
439
|
+
console.warn(' Skipping pre-commit hook installation. Please remove the symlink and re-run scaffold.');
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
} catch (e) {
|
|
443
|
+
// lstat failed; proceed (may be a permission issue)
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Ensure hooks directory exists
|
|
447
|
+
const gitHooksDir = path.join(gitDir, 'hooks');
|
|
448
|
+
if (!fs.existsSync(gitHooksDir)) {
|
|
449
|
+
fs.mkdirSync(gitHooksDir, { recursive: true });
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// SECURITY: Check if gitHooksDir is a symlink (refuse to install through symlinked dir)
|
|
453
|
+
try {
|
|
454
|
+
const hooksLstat = fs.lstatSync(gitHooksDir);
|
|
455
|
+
if (hooksLstat.isSymbolicLink()) {
|
|
456
|
+
console.warn('⚠ Warning: .git/hooks directory is a symlink (security risk)');
|
|
457
|
+
console.warn(' Skipping pre-commit hook installation. Please remove the symlink and re-run scaffold.');
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
} catch (e) {
|
|
461
|
+
// lstat failed; proceed (may be a permission issue)
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const waveguardSource = path.join(templateRoot, 'hooks', 'pre-commit-waveguard.sh');
|
|
465
|
+
const preCommitDest = path.join(gitHooksDir, 'pre-commit');
|
|
466
|
+
|
|
467
|
+
if (!fs.existsSync(waveguardSource)) {
|
|
468
|
+
// Waveguard source doesn't exist, skip
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Check if pre-commit hook already exists
|
|
473
|
+
if (fs.existsSync(preCommitDest)) {
|
|
474
|
+
// SECURITY: Check if existing pre-commit is a symlink (refuse to follow it)
|
|
475
|
+
try {
|
|
476
|
+
const destLstat = fs.lstatSync(preCommitDest);
|
|
477
|
+
if (destLstat.isSymbolicLink()) {
|
|
478
|
+
console.warn('⚠ Warning: Existing .git/hooks/pre-commit is a symlink (security risk)');
|
|
479
|
+
console.warn(' Skipping pre-commit hook installation. Please remove the symlink and re-run scaffold.');
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
} catch (e) {
|
|
483
|
+
// lstat failed; proceed
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Backup existing pre-commit hook
|
|
487
|
+
const backupPath = path.join(gitHooksDir, 'pre-commit.waveguard-backup');
|
|
488
|
+
if (!fs.existsSync(backupPath)) {
|
|
489
|
+
// Only backup if we haven't already
|
|
490
|
+
try {
|
|
491
|
+
const existingContent = fs.readFileSync(preCommitDest, 'utf8');
|
|
492
|
+
// Check if waveguard is already in the hook
|
|
493
|
+
if (existingContent.includes('pre-commit-waveguard')) {
|
|
494
|
+
console.log('✓ Pre-commit waveguard already installed (no changes)');
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
// Backup the existing hook
|
|
498
|
+
fs.copyFileSync(preCommitDest, backupPath);
|
|
499
|
+
console.log('✓ Backed up existing pre-commit hook to .git/hooks/pre-commit.waveguard-backup');
|
|
500
|
+
} catch (e) {
|
|
501
|
+
// Failed to backup, still try to install
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Create wrapper script that calls the waveguard hook
|
|
507
|
+
const wrapperScript = `#!/usr/bin/env bash
|
|
508
|
+
set -uo pipefail
|
|
509
|
+
|
|
510
|
+
repo_root=$(git rev-parse --show-toplevel 2>/dev/null || echo "")
|
|
511
|
+
if [ -z "$repo_root" ]; then
|
|
512
|
+
exit 1
|
|
513
|
+
fi
|
|
514
|
+
|
|
515
|
+
waveguard_hook="$repo_root/hooks/pre-commit-waveguard.sh"
|
|
516
|
+
if [ -f "$waveguard_hook" ]; then
|
|
517
|
+
bash "$waveguard_hook"
|
|
518
|
+
waveguard_exit=$?
|
|
519
|
+
if [ $waveguard_exit -ne 0 ]; then
|
|
520
|
+
exit $waveguard_exit
|
|
521
|
+
fi
|
|
522
|
+
fi
|
|
523
|
+
|
|
524
|
+
backup_hook="$repo_root/.git/hooks/pre-commit.waveguard-backup"
|
|
525
|
+
if [ -f "$backup_hook" ] && [ -x "$backup_hook" ]; then
|
|
526
|
+
bash "$backup_hook"
|
|
527
|
+
exit $?
|
|
528
|
+
fi
|
|
529
|
+
|
|
530
|
+
exit 0
|
|
531
|
+
`;
|
|
532
|
+
|
|
533
|
+
// Install the hook using copyFileSync (not symlink) to avoid dangling links
|
|
534
|
+
if (fs.existsSync(preCommitDest)) {
|
|
535
|
+
fs.unlinkSync(preCommitDest);
|
|
536
|
+
}
|
|
537
|
+
fs.writeFileSync(preCommitDest, wrapperScript);
|
|
538
|
+
console.log('✓ Installed pre-commit waveguard hook to .git/hooks/pre-commit');
|
|
539
|
+
|
|
540
|
+
// Ensure hook is executable
|
|
541
|
+
try {
|
|
542
|
+
fs.chmodSync(preCommitDest, 0o755);
|
|
543
|
+
} catch (e) {
|
|
544
|
+
// On Windows, chmod fails harmlessly; on POSIX, warn the user to chmod manually
|
|
545
|
+
if (process.platform !== 'win32') {
|
|
546
|
+
console.warn('⚠ Warning: Failed to chmod +x the pre-commit hook. Please run manually:');
|
|
547
|
+
console.warn(` chmod +x "${preCommitDest}"`);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
379
552
|
function installPrePushHook(targetDir, templateRoot) {
|
|
380
553
|
// Try to locate .git directory
|
|
381
554
|
const gitDir = path.join(targetDir, '.git');
|
|
@@ -460,36 +633,34 @@ function installPrePushHook(targetDir, templateRoot) {
|
|
|
460
633
|
}
|
|
461
634
|
|
|
462
635
|
// Install the hook
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
} else {
|
|
468
|
-
// Unix: symlink for easy updates
|
|
469
|
-
// First remove if exists
|
|
470
|
-
if (fs.existsSync(hookDest)) {
|
|
471
|
-
fs.unlinkSync(hookDest);
|
|
472
|
-
}
|
|
473
|
-
// Create symlink relative to .git/hooks/
|
|
474
|
-
const relPath = path.relative(gitHooksDir, hookSource);
|
|
475
|
-
fs.symlinkSync(relPath, hookDest);
|
|
476
|
-
console.log('✓ Symlinked pre-push policy hook to .git/hooks/pre-push');
|
|
636
|
+
// Use copyFileSync on all platforms to avoid dangling symlinks after npx cache clean
|
|
637
|
+
// which would disable branch protection and the secret gate
|
|
638
|
+
if (fs.existsSync(hookDest)) {
|
|
639
|
+
fs.unlinkSync(hookDest);
|
|
477
640
|
}
|
|
641
|
+
fs.copyFileSync(hookSource, hookDest);
|
|
642
|
+
console.log('✓ Copied pre-push policy hook to .git/hooks/pre-push');
|
|
478
643
|
|
|
479
644
|
// Ensure hook is executable
|
|
480
645
|
try {
|
|
481
646
|
fs.chmodSync(hookDest, 0o755);
|
|
482
647
|
} catch (e) {
|
|
483
|
-
// On Windows, chmod
|
|
648
|
+
// On Windows, chmod fails harmlessly; on POSIX, warn the user to chmod manually
|
|
649
|
+
if (process.platform !== 'win32') {
|
|
650
|
+
console.warn('⚠ Warning: Failed to chmod +x the pre-push hook. Please run manually:');
|
|
651
|
+
console.warn(` chmod +x "${hookDest}"`);
|
|
652
|
+
}
|
|
484
653
|
}
|
|
485
654
|
}
|
|
486
655
|
|
|
487
|
-
// Main execution function
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
656
|
+
// Main execution function (skip if runtime command detected)
|
|
657
|
+
if (!isRuntimeCommand) {
|
|
658
|
+
(async () => {
|
|
659
|
+
try {
|
|
660
|
+
let finalProjectName = projectName;
|
|
491
661
|
let finalReposStr = reposStr;
|
|
492
662
|
let finalDomainsStr = domainsStr;
|
|
663
|
+
let finalRepoUrlsStr = repoUrlsStr;
|
|
493
664
|
let finalTargetDir = targetDir;
|
|
494
665
|
let configPath = path.join(targetDir, 'aesop.config.json');
|
|
495
666
|
let dashboardPort = 8770;
|
|
@@ -597,11 +768,7 @@ function installPrePushHook(targetDir, templateRoot) {
|
|
|
597
768
|
console.warn(`⚠ Warning: aesop.config.json already exists at ${configPath}`);
|
|
598
769
|
} else {
|
|
599
770
|
// Generate aesop.config.json
|
|
600
|
-
const config = generateConfigJson(finalTargetDir, templateRoot, finalProjectName, finalReposStr);
|
|
601
|
-
// Update dashboard port if specified in wizard mode
|
|
602
|
-
if (wizardMode && dashboardPort !== 8770) {
|
|
603
|
-
config.dashboard.refresh_seconds = 1;
|
|
604
|
-
}
|
|
771
|
+
const config = generateConfigJson(finalTargetDir, templateRoot, finalProjectName, finalReposStr, finalRepoUrlsStr);
|
|
605
772
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
606
773
|
console.log('✓ Generated aesop.config.json (configured for your repos)');
|
|
607
774
|
}
|
|
@@ -618,6 +785,9 @@ function installPrePushHook(targetDir, templateRoot) {
|
|
|
618
785
|
// Install the pre-push hook
|
|
619
786
|
installPrePushHook(finalTargetDir, templateRoot);
|
|
620
787
|
|
|
788
|
+
// Install the pre-commit waveguard hook
|
|
789
|
+
installPreCommitWaveguard(finalTargetDir, templateRoot);
|
|
790
|
+
|
|
621
791
|
console.log(`\n✅ Scaffolded aesop template into "${finalTargetDir}" (${copiedCount} files)`);
|
|
622
792
|
|
|
623
793
|
if (wizardMode && wizardRl) {
|
|
@@ -629,7 +799,7 @@ function installPrePushHook(targetDir, templateRoot) {
|
|
|
629
799
|
console.log('\n🎯 Next 3 commands to get started:\n');
|
|
630
800
|
console.log(` 1. cd ${finalTargetDir}`);
|
|
631
801
|
console.log(' 2. bash daemons/run-watchdog.sh --once (one-time watchdog smoke test)');
|
|
632
|
-
console.log(` 3.
|
|
802
|
+
console.log(` 3. python3 ui/serve.py (or python as fallback; launch dashboard on localhost:${dashboardPort})`);
|
|
633
803
|
console.log('\n📖 After that, review:');
|
|
634
804
|
console.log(' • CLAUDE.md (pre-filled with your project info)');
|
|
635
805
|
console.log(' • aesop.config.json (pre-configured for your repos)');
|
|
@@ -646,7 +816,7 @@ function installPrePushHook(targetDir, templateRoot) {
|
|
|
646
816
|
console.log(` 6. cp ${finalTargetDir}/MEMORY-SEED.md ~/.claude/MEMORY.md (then add your facts)`);
|
|
647
817
|
console.log('\nRun the daemon and dashboard:');
|
|
648
818
|
console.log(' 7. bash daemons/run-watchdog.sh --once (test run)');
|
|
649
|
-
console.log(' 8.
|
|
819
|
+
console.log(' 8. python3 ui/serve.py (or python as fallback; launch dashboard on localhost:8770)');
|
|
650
820
|
} else {
|
|
651
821
|
console.log('\nConfiguration steps:');
|
|
652
822
|
console.log(` 1. cd ${finalTargetDir}`);
|
|
@@ -658,12 +828,13 @@ function installPrePushHook(targetDir, templateRoot) {
|
|
|
658
828
|
console.log(` 6. cp ${finalTargetDir}/MEMORY-SEED.md ~/.claude/MEMORY.md (then add your facts)`);
|
|
659
829
|
console.log('\nRun the daemon and dashboard:');
|
|
660
830
|
console.log(' 7. bash daemons/run-watchdog.sh --once (test run)');
|
|
661
|
-
console.log(' 8.
|
|
831
|
+
console.log(' 8. python3 ui/serve.py (or python as fallback; launch dashboard on localhost:8770)');
|
|
662
832
|
}
|
|
663
833
|
console.log('\nFor full documentation, see the README.md in the scaffolded directory.');
|
|
664
834
|
process.exit(0);
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
})();
|
|
835
|
+
} catch (err) {
|
|
836
|
+
console.error(`Error scaffolding template: ${err.message}`);
|
|
837
|
+
process.exit(1);
|
|
838
|
+
}
|
|
839
|
+
})();
|
|
840
|
+
}
|
package/daemons/CLAUDE.md
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
- Guards both daemon mode and `--once` mode (no bypass).
|
|
31
31
|
2. **Testing override**: Set `AESOP_WATCHDOG_CYCLE_CMD` env var to replace backup-fleet.sh invocation (allows tests to use mock cycle without running real backup).
|
|
32
32
|
3. **CRLF-safe, no line continuations**: Use POSIX-safe heredocs; never add `\` for line wrap.
|
|
33
|
-
4. **Secret-scan gate**: `scan_tracked_files()` calls `$AESOP_ROOT/tools/secret_scan.py` on
|
|
33
|
+
4. **Secret-scan gate**: `scan_tracked_files()` calls `$AESOP_ROOT/tools/secret_scan.py` on tracked modifications and untracked files; non-0 exit blocks push, marks repo BLOCKED.
|
|
34
34
|
5. **Append-only logs**: FLEET-BACKUP.log only grows; rotate via tools/rotate_logs.py.
|
|
35
35
|
6. **Path dedup via realpath**: Avoids processing symlinked repos or dot-dir aliases twice.
|
|
36
36
|
7. **Alert Bridge integration** (NEW wave-14): After each backup-fleet.sh cycle, run-watchdog.sh calls `python $AESOP_ROOT/tools/alert_bridge.py --scan || true` to post HIGH/CRITICAL security alerts and heartbeat staleness to Slack/Discord. No-op if webhook_url null/absent in aesop.config.json (feature opt-in). Uses cursor file for idempotent dispatch (no re-sends). See tools/alert_bridge.py for details.
|