@matt82198/aesop 0.1.0-beta.3 → 0.1.0-beta.5
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 +79 -95
- package/CLAUDE-TEMPLATE.md +115 -0
- package/README.md +126 -245
- package/aesop.config.example.json +34 -26
- package/bin/CLAUDE.md +67 -0
- package/bin/cli.js +593 -36
- package/daemons/CLAUDE.md +36 -0
- package/daemons/backup-fleet.sh +92 -21
- package/daemons/run-watchdog.sh +95 -12
- package/dash/CLAUDE.md +32 -0
- package/dash/dash-extra.mjs +194 -21
- package/dash/watchdog-gui.sh +23 -6
- package/docs/BEHAVIORAL-PR-REVIEW.md +86 -0
- package/docs/CHECKPOINTING.md +100 -0
- package/docs/DISPATCH-MODEL.md +11 -7
- package/docs/FORENSICS.md +204 -0
- package/docs/GOVERNANCE.md +4 -2
- package/docs/HOOK-INSTALL.md +307 -0
- package/docs/HOW-THE-LOOP-WORKS.md +175 -0
- package/docs/MEMORY-TEMPLATE.md +61 -0
- package/docs/README.md +44 -0
- package/docs/RELEASING.md +159 -0
- package/docs/RELIABILITY.md +61 -0
- package/docs/RESTORE.md +397 -0
- package/docs/SCRIPTS-POLICY.md +97 -0
- package/docs/archive/README.md +3 -0
- package/docs/archive/spikes/tiered-cognition/ACTIVATION.md +125 -0
- package/docs/archive/spikes/tiered-cognition/DESIGN.md +287 -0
- package/docs/archive/spikes/tiered-cognition/FINDINGS.md +113 -0
- package/docs/archive/spikes/tiered-cognition/README.md +27 -0
- package/docs/archive/spikes/tiered-cognition/aesop-cognition.example.md +32 -0
- package/docs/archive/spikes/tiered-cognition/force-model-policy.merged.mjs +673 -0
- package/docs/archive/spikes/tiered-cognition/strip-tools-hook.mjs +434 -0
- package/docs/case-study-portfolio.md +61 -0
- package/docs/self-stats-data.json +11 -0
- package/docs/templates/FLEET-OPS-ANALYSIS.example.md +27 -0
- package/docs/templates/FLEET-OPS-RECOMMENDATIONS.example.md +24 -0
- package/docs/templates/PROPOSALS-LOG.example.md +64 -0
- package/hooks/CLAUDE.md +61 -0
- package/hooks/claude/force-model-policy.mjs +159 -0
- package/hooks/pre-push-policy.sh +753 -0
- package/mcp/CLAUDE.md +213 -0
- package/mcp/package.json +26 -0
- package/mcp/server.mjs +543 -0
- package/monitor/.signal-state.json +3 -0
- package/monitor/ACTIONS.log +1 -0
- package/monitor/BRIEF.md +24 -0
- package/monitor/CHARTER.md +75 -88
- package/monitor/CLAUDE.md +40 -0
- package/monitor/SIGNALS.json +54 -0
- package/monitor/collect-signals.mjs +613 -61
- package/package.json +15 -3
- package/scan/fleet-scan.example.mjs +292 -0
- package/skills/CLAUDE.md +3 -0
- package/skills/healthcheck/SKILL.md +44 -0
- package/skills/power/SKILL.md +161 -0
- package/state_store/CLAUDE.md +39 -0
- package/state_store/__init__.py +24 -0
- 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/state_store/api.py +35 -0
- package/state_store/export.py +19 -0
- package/state_store/ingest.py +24 -0
- package/state_store/projections.py +52 -0
- package/state_store/store.py +102 -0
- package/tools/CLAUDE.md +72 -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/tools/agent-forensics.sh +186 -0
- package/tools/alert_bridge.py +449 -0
- package/tools/buildlog.py +182 -0
- package/tools/ci_merge_wait.py +317 -0
- package/tools/ensure_state.py +94 -0
- package/tools/eod_sweep.py +239 -0
- package/tools/fleet_ledger.py +272 -0
- package/tools/fleet_prompt_extractor.py +134 -0
- package/tools/healthcheck.py +296 -0
- package/tools/heartbeat.py +131 -0
- package/tools/inbox_drain.py +249 -0
- package/tools/launch_tui.py +51 -3
- package/tools/lock.mjs +211 -0
- package/tools/metrics_gate.py +205 -0
- package/tools/orchestrator_status.py +99 -0
- package/tools/power_selftest.py +386 -0
- package/tools/prepublish_scan.py +84 -0
- package/tools/proposals.mjs +248 -0
- package/tools/reconstitute.sh +467 -0
- package/tools/rotate_logs.py +228 -0
- package/tools/scanner_selftest.py +303 -0
- package/tools/secret_scan.py +138 -31
- package/tools/self_stats.py +509 -0
- package/tools/session_usage_summary.py +198 -0
- package/tools/stall_check.py +172 -0
- package/tools/svg_to_png.mjs +50 -0
- package/tools/transcript_replay.py +236 -0
- package/tools/transcript_timeline.py +184 -0
- package/tools/verify_dash.py +513 -0
- package/tools/verify_submit_encoding.py +202 -0
- package/ui/CLAUDE.md +125 -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/agents.py +183 -0
- package/ui/api/__init__.py +84 -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/api/submit.py +97 -0
- package/ui/api/tracker.py +116 -0
- package/ui/collectors.py +735 -0
- package/ui/config.py +123 -0
- package/ui/cost.py +260 -0
- package/ui/csrf.py +144 -0
- package/ui/handler.py +676 -0
- package/ui/render.py +49 -0
- package/ui/serve.py +68 -796
- package/ui/sse.py +183 -0
- package/ui/web/.gitattributes +13 -0
- package/ui/web/dist/assets/index-2LZDQirC.js +9 -0
- package/ui/web/dist/assets/index-D4M1qyOv.css +1 -0
- package/ui/web/dist/index.html +14 -0
- package/ui/web/index.html +13 -0
- package/ui/web/package-lock.json +2225 -0
- package/ui/web/package.json +26 -0
- package/ui/web/src/App.test.tsx +74 -0
- package/ui/web/src/App.tsx +142 -0
- package/ui/web/src/CONTRIBUTING-UI.md +49 -0
- package/ui/web/src/components/AgentRow.css +187 -0
- package/ui/web/src/components/AgentRow.test.tsx +209 -0
- package/ui/web/src/components/AgentRow.tsx +207 -0
- package/ui/web/src/components/AgentsPanel.css +108 -0
- package/ui/web/src/components/AgentsPanel.test.tsx +41 -0
- package/ui/web/src/components/AgentsPanel.tsx +58 -0
- package/ui/web/src/components/AlertsPanel.css +88 -0
- package/ui/web/src/components/AlertsPanel.test.tsx +51 -0
- package/ui/web/src/components/AlertsPanel.tsx +67 -0
- package/ui/web/src/components/BacklogPanel.test.tsx +126 -0
- package/ui/web/src/components/BacklogPanel.tsx +122 -0
- package/ui/web/src/components/CostChart.css +110 -0
- package/ui/web/src/components/CostChart.test.tsx +144 -0
- package/ui/web/src/components/CostChart.tsx +152 -0
- package/ui/web/src/components/CostTable.css +93 -0
- package/ui/web/src/components/CostTable.test.tsx +165 -0
- package/ui/web/src/components/CostTable.tsx +94 -0
- package/ui/web/src/components/EventsFeed.css +68 -0
- package/ui/web/src/components/EventsFeed.test.tsx +36 -0
- package/ui/web/src/components/EventsFeed.tsx +31 -0
- package/ui/web/src/components/HealthHeader.css +137 -0
- package/ui/web/src/components/HealthHeader.test.tsx +278 -0
- package/ui/web/src/components/HealthHeader.tsx +281 -0
- package/ui/web/src/components/InboxForm.css +135 -0
- package/ui/web/src/components/InboxForm.test.tsx +208 -0
- package/ui/web/src/components/InboxForm.tsx +116 -0
- package/ui/web/src/components/MessagesTail.module.css +144 -0
- package/ui/web/src/components/MessagesTail.test.tsx +176 -0
- package/ui/web/src/components/MessagesTail.tsx +94 -0
- package/ui/web/src/components/ReposPanel.css +90 -0
- package/ui/web/src/components/ReposPanel.test.tsx +45 -0
- package/ui/web/src/components/ReposPanel.tsx +67 -0
- package/ui/web/src/components/Scorecard.css +106 -0
- package/ui/web/src/components/Scorecard.test.tsx +117 -0
- package/ui/web/src/components/Scorecard.tsx +85 -0
- package/ui/web/src/components/Timeline.module.css +151 -0
- package/ui/web/src/components/Timeline.test.tsx +215 -0
- package/ui/web/src/components/Timeline.tsx +99 -0
- package/ui/web/src/components/TrackerBoard.test.tsx +121 -0
- package/ui/web/src/components/TrackerBoard.tsx +107 -0
- package/ui/web/src/components/TrackerCard.test.tsx +180 -0
- package/ui/web/src/components/TrackerCard.tsx +160 -0
- package/ui/web/src/components/TrackerForm.test.tsx +189 -0
- package/ui/web/src/components/TrackerForm.tsx +144 -0
- package/ui/web/src/lib/api.ts +218 -0
- package/ui/web/src/lib/format.test.ts +89 -0
- package/ui/web/src/lib/format.ts +103 -0
- package/ui/web/src/lib/sanitizeUrl.test.ts +84 -0
- package/ui/web/src/lib/sanitizeUrl.ts +38 -0
- package/ui/web/src/lib/types.ts +230 -0
- package/ui/web/src/lib/useHashRoute.test.ts +60 -0
- package/ui/web/src/lib/useHashRoute.ts +23 -0
- package/ui/web/src/lib/useSSE.ts +175 -0
- package/ui/web/src/main.tsx +10 -0
- package/ui/web/src/styles/global.css +179 -0
- package/ui/web/src/styles/theme.css +184 -0
- package/ui/web/src/styles/work.css +572 -0
- package/ui/web/src/test/fixtures.ts +385 -0
- package/ui/web/src/test/setup.ts +49 -0
- package/ui/web/src/views/Activity.module.css +43 -0
- package/ui/web/src/views/Activity.test.tsx +89 -0
- package/ui/web/src/views/Activity.tsx +31 -0
- package/ui/web/src/views/Cost.css +87 -0
- package/ui/web/src/views/Cost.test.tsx +142 -0
- package/ui/web/src/views/Cost.tsx +54 -0
- package/ui/web/src/views/Overview.css +51 -0
- package/ui/web/src/views/Overview.test.tsx +76 -0
- package/ui/web/src/views/Overview.tsx +46 -0
- package/ui/web/src/views/Work.test.tsx +82 -0
- package/ui/web/src/views/Work.tsx +79 -0
- package/ui/web/src/vite-env.d.ts +10 -0
- package/ui/web/tsconfig.json +22 -0
- package/ui/web/vite.config.ts +25 -0
- package/ui/web/vitest.config.ts +12 -0
package/bin/cli.js
CHANGED
|
@@ -2,52 +2,202 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const readline = require('readline');
|
|
7
|
+
const { execSync } = require('child_process');
|
|
5
8
|
|
|
6
9
|
const args = process.argv.slice(2);
|
|
7
10
|
const helpFlag = args.includes('--help') || args.includes('-h');
|
|
11
|
+
const forceFlag = args.includes('--force');
|
|
12
|
+
const yesFlag = args.includes('--yes');
|
|
13
|
+
|
|
14
|
+
// Detect if stdin is a TTY (interactive terminal)
|
|
15
|
+
function isInteractive() {
|
|
16
|
+
return process.stdin.isTTY && process.stdout.isTTY;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Discover git repositories under a base directory (non-recursive first level)
|
|
20
|
+
function discoverRepos(baseDir) {
|
|
21
|
+
const repos = [];
|
|
22
|
+
try {
|
|
23
|
+
if (!fs.existsSync(baseDir)) return repos;
|
|
24
|
+
const entries = fs.readdirSync(baseDir, { withFileTypes: true });
|
|
25
|
+
for (const entry of entries) {
|
|
26
|
+
if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
|
27
|
+
const fullPath = path.join(baseDir, entry.name);
|
|
28
|
+
const gitDir = path.join(fullPath, '.git');
|
|
29
|
+
if (fs.existsSync(gitDir)) {
|
|
30
|
+
repos.push({ name: entry.name, path: fullPath });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
} catch (e) {
|
|
35
|
+
// Silently ignore discovery errors
|
|
36
|
+
}
|
|
37
|
+
return repos;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Interactive prompt via readline (exported for testing)
|
|
41
|
+
async function promptUser(rl, prompt, defaultValue) {
|
|
42
|
+
return new Promise((resolve) => {
|
|
43
|
+
const displayPrompt = defaultValue ? `${prompt} (${defaultValue}): ` : `${prompt}: `;
|
|
44
|
+
rl.question(displayPrompt, (answer) => {
|
|
45
|
+
resolve(answer.trim() || defaultValue || '');
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Validate port is a number
|
|
51
|
+
function validatePort(port) {
|
|
52
|
+
const p = parseInt(port, 10);
|
|
53
|
+
return !isNaN(p) && p > 0 && p < 65536 ? p : null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Parse named flags
|
|
57
|
+
function getFlag(flagName) {
|
|
58
|
+
const idx = args.findIndex(arg => arg === flagName);
|
|
59
|
+
if (idx !== -1 && idx + 1 < args.length) {
|
|
60
|
+
return args[idx + 1];
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
8
64
|
|
|
9
65
|
if (helpFlag) {
|
|
10
66
|
console.log(`
|
|
11
67
|
aesop — Multi-agent orchestration template scaffolder
|
|
12
68
|
|
|
13
69
|
Usage:
|
|
14
|
-
npx @matt82198/aesop [target-dir]
|
|
70
|
+
npx @matt82198/aesop [target-dir] [options]
|
|
71
|
+
npx @matt82198/aesop wizard [options]
|
|
72
|
+
|
|
73
|
+
Commands:
|
|
74
|
+
wizard Interactive onboarding (prompts for project name, repos, port)
|
|
15
75
|
|
|
16
76
|
Arguments:
|
|
17
77
|
target-dir Directory to scaffold the template into (default: "aesop-fleet")
|
|
18
78
|
|
|
79
|
+
Options:
|
|
80
|
+
--help, -h Show this help message
|
|
81
|
+
--force Replace any existing .git/hooks/pre-push during scaffold
|
|
82
|
+
--yes Skip interactive prompts, use defaults (CI-safe)
|
|
83
|
+
--name <name> Project name (for headless scaffolding; generates CLAUDE.md + aesop.config.json)
|
|
84
|
+
--domains <list> Comma-separated domain list (e.g., "api,worker,monitoring")
|
|
85
|
+
--repos <paths> Comma-separated repo paths (e.g., "/path/to/repo1,/path/to/repo2")
|
|
86
|
+
|
|
19
87
|
Examples:
|
|
20
|
-
npx @matt82198/aesop
|
|
21
|
-
npx @matt82198/aesop my-fleet
|
|
22
|
-
npx @matt82198/aesop
|
|
88
|
+
npx @matt82198/aesop # Creates ./aesop-fleet/ with template
|
|
89
|
+
npx @matt82198/aesop my-fleet # Creates ./my-fleet/ with template
|
|
90
|
+
npx @matt82198/aesop wizard # Interactive onboarding (60-second setup)
|
|
91
|
+
npx @matt82198/aesop my-fleet --force # Re-scaffold and replace hooks
|
|
92
|
+
npx @matt82198/aesop orchestrator --name "my-service" # Headless: generates CLAUDE.md + config
|
|
23
93
|
|
|
24
|
-
|
|
25
|
-
|
|
94
|
+
Full headless with domains and repos (POSIX):
|
|
95
|
+
npx @matt82198/aesop orchestrator --name "api" --domains "server,worker" --repos "/path/to/api"
|
|
96
|
+
|
|
97
|
+
Full headless with domains and repos (PowerShell):
|
|
98
|
+
npx @matt82198/aesop orchestrator --name "api" --domains "server,worker" --repos "C:\path\to\api"
|
|
99
|
+
|
|
100
|
+
Interactive wizard flow:
|
|
101
|
+
1. Run: npx @matt82198/aesop wizard
|
|
102
|
+
2. Answer prompts (press Enter for defaults): project name, repos to watch, port, brain root
|
|
103
|
+
3. Scaffolds template, writes aesop.config.json, prints next steps
|
|
104
|
+
4. Optionally runs watchdog smoke test
|
|
26
105
|
|
|
27
106
|
After scaffolding, cd into the directory and:
|
|
28
|
-
1.
|
|
29
|
-
2.
|
|
107
|
+
1. Review CLAUDE.md (pre-filled with your project info)
|
|
108
|
+
2. Review aesop.config.json (pre-configured for your repos)
|
|
30
109
|
3. Run: bash daemons/run-watchdog.sh --once
|
|
31
110
|
4. Launch dashboard: python ui/serve.py
|
|
32
111
|
`);
|
|
33
112
|
process.exit(0);
|
|
34
113
|
}
|
|
35
114
|
|
|
36
|
-
|
|
115
|
+
// Check if wizard mode is requested (either as first arg or after targetDir)
|
|
116
|
+
// e.g., "aesop wizard" or "aesop my-dir wizard"
|
|
117
|
+
let wizardModeRequested = false;
|
|
118
|
+
let wizardArgIndex = -1;
|
|
119
|
+
|
|
120
|
+
// Check if 'wizard' is in the args
|
|
121
|
+
for (let i = 0; i < args.length; i++) {
|
|
122
|
+
if (args[i] === 'wizard') {
|
|
123
|
+
wizardModeRequested = true;
|
|
124
|
+
wizardArgIndex = i;
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const wizardMode = wizardModeRequested || (args.length === 0 && isInteractive());
|
|
37
130
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
131
|
+
// Handle wizard mode by removing 'wizard' from args
|
|
132
|
+
if (wizardModeRequested && wizardArgIndex >= 0) {
|
|
133
|
+
args.splice(wizardArgIndex, 1);
|
|
41
134
|
}
|
|
42
135
|
|
|
43
|
-
//
|
|
136
|
+
// Extract targetDir (first non-flag argument, excluding flag values)
|
|
137
|
+
// Build set of indices consumed as flag values (tokens after --name/--domains/--repos)
|
|
138
|
+
const consumedIndices = new Set();
|
|
139
|
+
const flagsWithValues = ['--name', '--domains', '--repos'];
|
|
140
|
+
for (let i = 0; i < args.length; i++) {
|
|
141
|
+
if (flagsWithValues.includes(args[i]) && i + 1 < args.length) {
|
|
142
|
+
consumedIndices.add(i + 1);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// targetDir = first non-flag, non-consumed argument, else default to 'aesop-fleet'
|
|
146
|
+
const targetDir = args.find((arg, idx) =>
|
|
147
|
+
!arg.startsWith('--') &&
|
|
148
|
+
!arg.startsWith('-') &&
|
|
149
|
+
!consumedIndices.has(idx)
|
|
150
|
+
) || 'aesop-fleet';
|
|
151
|
+
|
|
152
|
+
// Parse onboarding flags
|
|
153
|
+
const projectName = getFlag('--name');
|
|
154
|
+
const domainsStr = getFlag('--domains');
|
|
155
|
+
const reposStr = getFlag('--repos');
|
|
156
|
+
|
|
157
|
+
// Validate target directory doesn't exist or is empty (except for .git and aesop files)
|
|
44
158
|
if (fs.existsSync(targetDir)) {
|
|
45
159
|
const contents = fs.readdirSync(targetDir);
|
|
46
|
-
|
|
47
|
-
|
|
160
|
+
// Allow .git and aesop scaffolded files to already exist (for idempotency)
|
|
161
|
+
const aesopDirs = ['daemons', 'dash', 'monitor', 'tools', 'ui', 'docs', '.git', 'state'];
|
|
162
|
+
const aesopFiles = [
|
|
163
|
+
'aesop.config.example.json',
|
|
164
|
+
'aesop.config.json',
|
|
165
|
+
'README.md',
|
|
166
|
+
'LICENSE',
|
|
167
|
+
'CHANGELOG.md',
|
|
168
|
+
'CLAUDE-TEMPLATE.md',
|
|
169
|
+
'CLAUDE.md',
|
|
170
|
+
'MEMORY-SEED.md'
|
|
171
|
+
];
|
|
172
|
+
const allowedItems = new Set([...aesopDirs, ...aesopFiles]);
|
|
173
|
+
|
|
174
|
+
const unexpectedContents = contents.filter(item => !allowedItems.has(item));
|
|
175
|
+
if (unexpectedContents.length > 0) {
|
|
176
|
+
console.error(`Error: Directory "${targetDir}" exists and contains unexpected files.`);
|
|
48
177
|
console.error('Please choose a different target directory or remove the existing one.');
|
|
49
178
|
process.exit(1);
|
|
50
179
|
}
|
|
180
|
+
|
|
181
|
+
// SECURITY: A pre-existing allowlisted entry (e.g. ".git", "state") must be a real
|
|
182
|
+
// directory/file, never a symlink or junction. Following a symlinked entry here would
|
|
183
|
+
// let scaffolding escape targetDir entirely (e.g. a symlinked .git whose "hooks"
|
|
184
|
+
// subdirectory resolves outside targetDir). lstat (not stat) so the link itself is
|
|
185
|
+
// inspected rather than whatever it points to.
|
|
186
|
+
for (const item of contents) {
|
|
187
|
+
const itemPath = path.join(targetDir, item);
|
|
188
|
+
let itemLstat;
|
|
189
|
+
try {
|
|
190
|
+
itemLstat = fs.lstatSync(itemPath);
|
|
191
|
+
} catch (e) {
|
|
192
|
+
// lstat failed (race/permissions); nothing to check, move on
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (itemLstat.isSymbolicLink()) {
|
|
196
|
+
console.error(`Error: "${item}" in "${targetDir}" is a symlink (security risk).`);
|
|
197
|
+
console.error('Refusing to scaffold through a symlinked entry. Remove it and re-run.');
|
|
198
|
+
process.exit(1);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
51
201
|
} else {
|
|
52
202
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
53
203
|
}
|
|
@@ -64,7 +214,8 @@ const filesToCopy = [
|
|
|
64
214
|
'aesop.config.example.json',
|
|
65
215
|
'README.md',
|
|
66
216
|
'LICENSE',
|
|
67
|
-
'CHANGELOG.md'
|
|
217
|
+
'CHANGELOG.md',
|
|
218
|
+
'CLAUDE-TEMPLATE.md'
|
|
68
219
|
];
|
|
69
220
|
|
|
70
221
|
let copiedCount = 0;
|
|
@@ -87,26 +238,432 @@ function copyRecursive(src, dest) {
|
|
|
87
238
|
}
|
|
88
239
|
}
|
|
89
240
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
241
|
+
function generateDomainText(domainsStr) {
|
|
242
|
+
if (!domainsStr) {
|
|
243
|
+
return '- **[Your domains here]** — [Customize per team]';
|
|
244
|
+
}
|
|
245
|
+
const domains = domainsStr.split(',').map(d => d.trim()).filter(d => d);
|
|
246
|
+
if (domains.length === 0) {
|
|
247
|
+
return '- **[Your domains here]** — [Customize per team]';
|
|
248
|
+
}
|
|
249
|
+
return domains
|
|
250
|
+
.map(domain => `- **${domain}/** — [Add description for ${domain}]`)
|
|
251
|
+
.join('\n');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function generateRepoText(reposStr) {
|
|
255
|
+
if (!reposStr) {
|
|
256
|
+
return '- **[Your repos here]** — [Customize per team]';
|
|
257
|
+
}
|
|
258
|
+
const repos = reposStr.split(',').map(r => r.trim()).filter(r => r);
|
|
259
|
+
if (repos.length === 0) {
|
|
260
|
+
return '- **[Your repos here]** — [Customize per team]';
|
|
261
|
+
}
|
|
262
|
+
return repos
|
|
263
|
+
.map(repo => {
|
|
264
|
+
const repoName = path.basename(repo);
|
|
265
|
+
return `- **${repo}** — [${repoName} domains and setup]`;
|
|
266
|
+
})
|
|
267
|
+
.join('\n');
|
|
268
|
+
}
|
|
99
269
|
|
|
100
|
-
|
|
101
|
-
|
|
270
|
+
function substituteTemplate(templateContent, projectName, domainsStr, reposStr) {
|
|
271
|
+
let result = templateContent;
|
|
272
|
+
result = result.replace(/{{PROJECT_NAME}}/g, projectName || 'my-fleet');
|
|
273
|
+
const domainList = domainsStr ? domainsStr.split(',').map(d => d.trim()).join(', ') : 'multi-domain';
|
|
274
|
+
result = result.replace(/{{DOMAIN_LIST}}/g, domainList);
|
|
275
|
+
result = result.replace(/{{DOMAINS}}/g, generateDomainText(domainsStr));
|
|
276
|
+
result = result.replace(/{{REPO_LIST}}/g, generateRepoText(reposStr));
|
|
277
|
+
return result;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function generateConfigJson(targetDir, templateRoot, projectName, reposStr) {
|
|
281
|
+
// Read example config
|
|
282
|
+
const exampleConfigPath = path.join(templateRoot, 'aesop.config.example.json');
|
|
283
|
+
let exampleConfig;
|
|
284
|
+
try {
|
|
285
|
+
const content = fs.readFileSync(exampleConfigPath, 'utf8');
|
|
286
|
+
exampleConfig = JSON.parse(content);
|
|
287
|
+
} catch (e) {
|
|
288
|
+
// Fallback config if example doesn't parse
|
|
289
|
+
exampleConfig = {
|
|
290
|
+
description: 'Aesop configuration',
|
|
291
|
+
aesop_root: path.dirname(targetDir),
|
|
292
|
+
brain_root: path.join(os.homedir(), '.claude'),
|
|
293
|
+
scripts_root: path.join(os.homedir(), 'scripts'),
|
|
294
|
+
temp_root: path.join(os.tmpdir()),
|
|
295
|
+
repos: [],
|
|
296
|
+
watchdog: {
|
|
297
|
+
cycle_seconds: 150,
|
|
298
|
+
heartbeat_threshold_seconds: 200,
|
|
299
|
+
enable_secret_scan: true,
|
|
300
|
+
secret_scan_script: 'tools/secret_scan.py'
|
|
301
|
+
},
|
|
302
|
+
monitor: {
|
|
303
|
+
enable: true,
|
|
304
|
+
cycle_seconds: 3600,
|
|
305
|
+
heartbeat_threshold_seconds: 3600,
|
|
306
|
+
memory_staleness_days: 30,
|
|
307
|
+
log_max_lines: 500,
|
|
308
|
+
log_max_kb: 40
|
|
309
|
+
},
|
|
310
|
+
dashboard: {
|
|
311
|
+
refresh_seconds: 1,
|
|
312
|
+
enable_jq_parsing: true,
|
|
313
|
+
theme: 'dark'
|
|
314
|
+
},
|
|
315
|
+
cardinal_rules: {
|
|
316
|
+
subagent_model: 'haiku',
|
|
317
|
+
orchestrator_model: 'opus',
|
|
318
|
+
tdd_first: true,
|
|
319
|
+
never_push_main: true,
|
|
320
|
+
secret_scan_gates_push: true
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Update with provided values
|
|
326
|
+
exampleConfig.aesop_root = targetDir;
|
|
327
|
+
exampleConfig.brain_root = '~/.claude';
|
|
328
|
+
exampleConfig.scripts_root = '~/scripts';
|
|
329
|
+
exampleConfig.temp_root = '~/.aesop-temp';
|
|
330
|
+
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
|
+
|
|
332
|
+
// Parse repos if provided
|
|
333
|
+
if (reposStr) {
|
|
334
|
+
const repos = reposStr.split(',').map(r => r.trim()).filter(r => r);
|
|
335
|
+
exampleConfig.repos = repos.map((repoPath, idx) => ({
|
|
336
|
+
path: repoPath,
|
|
337
|
+
name: path.basename(repoPath),
|
|
338
|
+
url: `https://github.com/user/${path.basename(repoPath)}.git`,
|
|
339
|
+
primary_branch: 'main',
|
|
340
|
+
backup_branch: 'backup/wip'
|
|
341
|
+
}));
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return exampleConfig;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Print next steps and optionally run watchdog (returns a Promise)
|
|
348
|
+
async function printNextStepsAndWatchdog(rl, targetDir, configPath, port) {
|
|
349
|
+
console.log('\n🎯 Next 3 commands to get started:\n');
|
|
102
350
|
console.log(` 1. cd ${targetDir}`);
|
|
103
|
-
console.log(' 2.
|
|
104
|
-
console.log(
|
|
105
|
-
console.log('
|
|
106
|
-
console.log('
|
|
107
|
-
console.log('
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
351
|
+
console.log(' 2. bash daemons/run-watchdog.sh --once (one-time watchdog smoke test)');
|
|
352
|
+
console.log(` 3. python ui/serve.py (launch dashboard on localhost:${port})`);
|
|
353
|
+
console.log('\n📖 After that, review:');
|
|
354
|
+
console.log(' • CLAUDE.md (pre-filled with your project info)');
|
|
355
|
+
console.log(' • aesop.config.json (pre-configured for your repos)');
|
|
356
|
+
console.log(' • docs/MEMORY-TEMPLATE.md (edit ~/.claude/MEMORY.md with your facts)\n');
|
|
357
|
+
|
|
358
|
+
return new Promise((resolve) => {
|
|
359
|
+
rl.question('Run watchdog --once now? (y/N): ', (answer) => {
|
|
360
|
+
if (answer.toLowerCase() === 'y') {
|
|
361
|
+
console.log('\nRunning watchdog smoke test...');
|
|
362
|
+
try {
|
|
363
|
+
const watchdogScript = path.join(targetDir, 'daemons', 'run-watchdog.sh');
|
|
364
|
+
if (fs.existsSync(watchdogScript)) {
|
|
365
|
+
// Use bash to run the script
|
|
366
|
+
execSync(`bash "${watchdogScript}" --once`, { stdio: 'inherit', cwd: targetDir });
|
|
367
|
+
console.log('\n✓ Watchdog smoke test completed');
|
|
368
|
+
}
|
|
369
|
+
} catch (e) {
|
|
370
|
+
console.error('\n⚠ Watchdog test failed (this is OK, continue manually)');
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
rl.close();
|
|
374
|
+
resolve();
|
|
375
|
+
});
|
|
376
|
+
});
|
|
112
377
|
}
|
|
378
|
+
|
|
379
|
+
function installPrePushHook(targetDir, templateRoot) {
|
|
380
|
+
// Try to locate .git directory
|
|
381
|
+
const gitDir = path.join(targetDir, '.git');
|
|
382
|
+
if (!fs.existsSync(gitDir)) {
|
|
383
|
+
// No git repo, skip hook installation silently
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// SECURITY: Check if .git itself is a symlink/junction (refuse to follow it outside
|
|
388
|
+
// targetDir). This mirrors the gitHooksDir/hookDest checks below — .git is one path
|
|
389
|
+
// component higher and must be validated before anything derived from it is trusted.
|
|
390
|
+
try {
|
|
391
|
+
const gitDirLstat = fs.lstatSync(gitDir);
|
|
392
|
+
if (gitDirLstat.isSymbolicLink()) {
|
|
393
|
+
console.warn('⚠ Warning: .git is a symlink (security risk)');
|
|
394
|
+
console.warn(' Skipping hook installation. Please remove the symlink and re-run scaffold.');
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
} catch (e) {
|
|
398
|
+
// lstat failed; proceed (may be a permission issue)
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Ensure hooks directory exists
|
|
402
|
+
const gitHooksDir = path.join(gitDir, 'hooks');
|
|
403
|
+
if (!fs.existsSync(gitHooksDir)) {
|
|
404
|
+
fs.mkdirSync(gitHooksDir, { recursive: true });
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// SECURITY: Check if gitHooksDir is a symlink (refuse to install through symlinked dir)
|
|
408
|
+
try {
|
|
409
|
+
const hooksLstat = fs.lstatSync(gitHooksDir);
|
|
410
|
+
if (hooksLstat.isSymbolicLink()) {
|
|
411
|
+
console.warn('⚠ Warning: .git/hooks directory is a symlink (security risk)');
|
|
412
|
+
console.warn(' Skipping hook installation. Please remove the symlink and re-run scaffold.');
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
} catch (e) {
|
|
416
|
+
// lstat failed; proceed (may be a permission issue)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const hookSource = path.join(templateRoot, 'hooks', 'pre-push-policy.sh');
|
|
420
|
+
const hookDest = path.join(gitHooksDir, 'pre-push');
|
|
421
|
+
|
|
422
|
+
if (!fs.existsSync(hookSource)) {
|
|
423
|
+
// Hook source doesn't exist, skip
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const hookSourceContent = fs.readFileSync(hookSource, 'utf8');
|
|
428
|
+
|
|
429
|
+
// Check if hook already exists
|
|
430
|
+
if (fs.existsSync(hookDest)) {
|
|
431
|
+
// SECURITY: Check if existing hookDest is a symlink (refuse to follow it)
|
|
432
|
+
try {
|
|
433
|
+
const destLstat = fs.lstatSync(hookDest);
|
|
434
|
+
if (destLstat.isSymbolicLink()) {
|
|
435
|
+
console.warn('⚠ Warning: Existing .git/hooks/pre-push is a symlink (security risk)');
|
|
436
|
+
console.warn(' Skipping hook installation. Please remove the symlink and re-run scaffold.');
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
} catch (e) {
|
|
440
|
+
// lstat failed; proceed
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const existingContent = fs.readFileSync(hookDest, 'utf8');
|
|
444
|
+
|
|
445
|
+
// If content matches, it's idempotent, do nothing
|
|
446
|
+
if (existingContent === hookSourceContent) {
|
|
447
|
+
console.log('✓ Pre-push hook already installed (no changes)');
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Different hook exists
|
|
452
|
+
if (!forceFlag) {
|
|
453
|
+
console.warn('⚠ Warning: A different pre-push hook already exists at ' + hookDest);
|
|
454
|
+
console.warn(' Use --force to replace it, or customize manually.');
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// --force: replace the existing hook
|
|
459
|
+
console.log('✓ Replacing existing pre-push hook with --force');
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Install the hook
|
|
463
|
+
if (process.platform === 'win32') {
|
|
464
|
+
// Windows: copy the file
|
|
465
|
+
fs.copyFileSync(hookSource, hookDest);
|
|
466
|
+
console.log('✓ Copied pre-push policy hook to .git/hooks/pre-push');
|
|
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');
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Ensure hook is executable
|
|
480
|
+
try {
|
|
481
|
+
fs.chmodSync(hookDest, 0o755);
|
|
482
|
+
} catch (e) {
|
|
483
|
+
// On Windows, chmod may fail; that's okay
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Main execution function
|
|
488
|
+
(async () => {
|
|
489
|
+
try {
|
|
490
|
+
let finalProjectName = projectName;
|
|
491
|
+
let finalReposStr = reposStr;
|
|
492
|
+
let finalDomainsStr = domainsStr;
|
|
493
|
+
let finalTargetDir = targetDir;
|
|
494
|
+
let configPath = path.join(targetDir, 'aesop.config.json');
|
|
495
|
+
let dashboardPort = 8770;
|
|
496
|
+
|
|
497
|
+
let wizardRl = null;
|
|
498
|
+
|
|
499
|
+
// Handle wizard mode
|
|
500
|
+
if (wizardMode && isInteractive() && !yesFlag) {
|
|
501
|
+
wizardRl = readline.createInterface({
|
|
502
|
+
input: process.stdin,
|
|
503
|
+
output: process.stdout
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
console.log('\n🪄 Aesop interactive onboarding wizard');
|
|
507
|
+
console.log('═'.repeat(50));
|
|
508
|
+
console.log('Answer these 4 questions to set up your fleet in 60 seconds.');
|
|
509
|
+
console.log('Press Enter to accept defaults (shown in parentheses).\n');
|
|
510
|
+
|
|
511
|
+
// Q1: Project name
|
|
512
|
+
finalProjectName = await promptUser(wizardRl, 'Project name', 'my-fleet');
|
|
513
|
+
|
|
514
|
+
// Q2: Repos to watch
|
|
515
|
+
console.log('\nDiscovering git repos in your home directory...');
|
|
516
|
+
const discoveredRepos = discoverRepos(os.homedir());
|
|
517
|
+
let selectedRepos = '';
|
|
518
|
+
|
|
519
|
+
if (discoveredRepos.length > 0) {
|
|
520
|
+
console.log(`Found ${discoveredRepos.length} git repo(s):\n`);
|
|
521
|
+
discoveredRepos.forEach((repo, i) => {
|
|
522
|
+
console.log(` ${i + 1}. ${repo.name} (${repo.path})`);
|
|
523
|
+
});
|
|
524
|
+
console.log('\nEnter repo paths to watch (comma-separated), or press Enter to skip:');
|
|
525
|
+
selectedRepos = await promptUser(wizardRl, 'Repos', '');
|
|
526
|
+
if (!selectedRepos && discoveredRepos.length > 0) {
|
|
527
|
+
// Auto-select the first discovered repo if user presses Enter
|
|
528
|
+
selectedRepos = discoveredRepos[0].path;
|
|
529
|
+
console.log(` → Using: ${selectedRepos}`);
|
|
530
|
+
}
|
|
531
|
+
} else {
|
|
532
|
+
selectedRepos = await promptUser(wizardRl, 'Repos to watch (paths, comma-separated)', '');
|
|
533
|
+
}
|
|
534
|
+
finalReposStr = selectedRepos;
|
|
535
|
+
|
|
536
|
+
// Q3: Dashboard port
|
|
537
|
+
let port = '';
|
|
538
|
+
while (!port) {
|
|
539
|
+
port = await promptUser(wizardRl, 'Dashboard port', '8770');
|
|
540
|
+
const validPort = validatePort(port);
|
|
541
|
+
if (!validPort) {
|
|
542
|
+
console.log(' ✗ Invalid port. Must be a number between 1 and 65535.');
|
|
543
|
+
port = '';
|
|
544
|
+
} else {
|
|
545
|
+
dashboardPort = validPort;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Q4: Brain root
|
|
550
|
+
const brainRoot = await promptUser(wizardRl, 'Brain root directory', '~/.claude');
|
|
551
|
+
|
|
552
|
+
console.log('\n✨ Scaffolding your fleet...\n');
|
|
553
|
+
|
|
554
|
+
// Update config path for wizard-generated config
|
|
555
|
+
configPath = path.join(finalTargetDir, 'aesop.config.json');
|
|
556
|
+
} else if (yesFlag && wizardMode) {
|
|
557
|
+
// Non-interactive defaults for wizard mode with --yes
|
|
558
|
+
finalProjectName = 'my-fleet';
|
|
559
|
+
finalReposStr = '';
|
|
560
|
+
dashboardPort = 8770;
|
|
561
|
+
console.log('🪄 Running onboarding wizard with defaults (--yes)');
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Copy template files
|
|
565
|
+
filesToCopy.forEach(item => {
|
|
566
|
+
const src = path.join(templateRoot, item);
|
|
567
|
+
const dest = path.join(finalTargetDir, item);
|
|
568
|
+
if (fs.existsSync(src)) {
|
|
569
|
+
copyRecursive(src, dest);
|
|
570
|
+
console.log(`✓ Copied ${item}`);
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
// Create state/ directory
|
|
575
|
+
const stateDir = path.join(finalTargetDir, 'state');
|
|
576
|
+
if (!fs.existsSync(stateDir)) {
|
|
577
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
578
|
+
console.log('✓ Created state/ directory');
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// If --name or wizard provided, generate CLAUDE.md and aesop.config.json
|
|
582
|
+
if (finalProjectName) {
|
|
583
|
+
// Generate CLAUDE.md from template
|
|
584
|
+
const templatePath = path.join(finalTargetDir, 'CLAUDE-TEMPLATE.md');
|
|
585
|
+
const claudeMdPath = path.join(finalTargetDir, 'CLAUDE.md');
|
|
586
|
+
if (fs.existsSync(templatePath)) {
|
|
587
|
+
const templateContent = fs.readFileSync(templatePath, 'utf8');
|
|
588
|
+
const claudeContent = substituteTemplate(templateContent, finalProjectName, finalDomainsStr, finalReposStr);
|
|
589
|
+
fs.writeFileSync(claudeMdPath, claudeContent);
|
|
590
|
+
console.log('✓ Generated CLAUDE.md (from template with substitutions)');
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Check if aesop.config.json already exists (for wizard mode)
|
|
594
|
+
if (fs.existsSync(configPath) && wizardMode && isInteractive()) {
|
|
595
|
+
// This should not happen in wizard mode since we're creating a new targetDir
|
|
596
|
+
// But if it does, warn and skip
|
|
597
|
+
console.warn(`⚠ Warning: aesop.config.json already exists at ${configPath}`);
|
|
598
|
+
} else {
|
|
599
|
+
// 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
|
+
}
|
|
605
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
606
|
+
console.log('✓ Generated aesop.config.json (configured for your repos)');
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// Copy MEMORY-TEMPLATE.md as MEMORY-SEED.md
|
|
611
|
+
const memoryTemplatePath = path.join(templateRoot, 'docs', 'MEMORY-TEMPLATE.md');
|
|
612
|
+
const memorySeedPath = path.join(finalTargetDir, 'MEMORY-SEED.md');
|
|
613
|
+
if (fs.existsSync(memoryTemplatePath) && !fs.existsSync(memorySeedPath)) {
|
|
614
|
+
fs.copyFileSync(memoryTemplatePath, memorySeedPath);
|
|
615
|
+
console.log('✓ Copied MEMORY-SEED.md (template for your facts)');
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Install the pre-push hook
|
|
619
|
+
installPrePushHook(finalTargetDir, templateRoot);
|
|
620
|
+
|
|
621
|
+
console.log(`\n✅ Scaffolded aesop template into "${finalTargetDir}" (${copiedCount} files)`);
|
|
622
|
+
|
|
623
|
+
if (wizardMode && wizardRl) {
|
|
624
|
+
// Wizard mode: print next steps and offer to run watchdog
|
|
625
|
+
await printNextStepsAndWatchdog(wizardRl, finalTargetDir, configPath, dashboardPort);
|
|
626
|
+
process.exit(0);
|
|
627
|
+
} else if (wizardMode) {
|
|
628
|
+
// Wizard mode with --yes flag (non-interactive)
|
|
629
|
+
console.log('\n🎯 Next 3 commands to get started:\n');
|
|
630
|
+
console.log(` 1. cd ${finalTargetDir}`);
|
|
631
|
+
console.log(' 2. bash daemons/run-watchdog.sh --once (one-time watchdog smoke test)');
|
|
632
|
+
console.log(` 3. python ui/serve.py (launch dashboard on localhost:${dashboardPort})`);
|
|
633
|
+
console.log('\n📖 After that, review:');
|
|
634
|
+
console.log(' • CLAUDE.md (pre-filled with your project info)');
|
|
635
|
+
console.log(' • aesop.config.json (pre-configured for your repos)');
|
|
636
|
+
console.log(' • docs/MEMORY-TEMPLATE.md (edit ~/.claude/MEMORY.md with your facts)');
|
|
637
|
+
process.exit(0);
|
|
638
|
+
} else if (finalProjectName) {
|
|
639
|
+
console.log('\nHeadless scaffolding complete! Next steps:');
|
|
640
|
+
console.log(` 1. cd ${finalTargetDir}`);
|
|
641
|
+
console.log(' 2. Review CLAUDE.md (pre-filled with your project info)');
|
|
642
|
+
console.log(' 3. Review aesop.config.json (pre-configured for your repos)');
|
|
643
|
+
console.log('\nInitialize your brain (Claude Code team memory):');
|
|
644
|
+
console.log(' 4. mkdir -p ~/.claude/memory');
|
|
645
|
+
console.log(` 5. cp ${finalTargetDir}/CLAUDE.md ~/.claude/CLAUDE.md (or review existing ~/.claude/CLAUDE.md)`);
|
|
646
|
+
console.log(` 6. cp ${finalTargetDir}/MEMORY-SEED.md ~/.claude/MEMORY.md (then add your facts)`);
|
|
647
|
+
console.log('\nRun the daemon and dashboard:');
|
|
648
|
+
console.log(' 7. bash daemons/run-watchdog.sh --once (test run)');
|
|
649
|
+
console.log(' 8. python ui/serve.py (launch dashboard on localhost:8770)');
|
|
650
|
+
} else {
|
|
651
|
+
console.log('\nConfiguration steps:');
|
|
652
|
+
console.log(` 1. cd ${finalTargetDir}`);
|
|
653
|
+
console.log(' 2. cp aesop.config.example.json aesop.config.json');
|
|
654
|
+
console.log(' 3. Edit aesop.config.json with your configuration');
|
|
655
|
+
console.log('\nInitialize your brain (Claude Code team memory):');
|
|
656
|
+
console.log(' 4. mkdir -p ~/.claude/memory');
|
|
657
|
+
console.log(` 5. cp ${finalTargetDir}/CLAUDE-TEMPLATE.md ~/.claude/CLAUDE.md (then edit domains/team info)`);
|
|
658
|
+
console.log(` 6. cp ${finalTargetDir}/MEMORY-SEED.md ~/.claude/MEMORY.md (then add your facts)`);
|
|
659
|
+
console.log('\nRun the daemon and dashboard:');
|
|
660
|
+
console.log(' 7. bash daemons/run-watchdog.sh --once (test run)');
|
|
661
|
+
console.log(' 8. python ui/serve.py (launch dashboard on localhost:8770)');
|
|
662
|
+
}
|
|
663
|
+
console.log('\nFor full documentation, see the README.md in the scaffolded directory.');
|
|
664
|
+
process.exit(0);
|
|
665
|
+
} catch (err) {
|
|
666
|
+
console.error(`Error scaffolding template: ${err.message}`);
|
|
667
|
+
process.exit(1);
|
|
668
|
+
}
|
|
669
|
+
})();
|