brain-dev 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +152 -0
  3. package/agents/brain-checker.md +33 -0
  4. package/agents/brain-debugger.md +35 -0
  5. package/agents/brain-executor.md +37 -0
  6. package/agents/brain-mapper.md +44 -0
  7. package/agents/brain-planner.md +49 -0
  8. package/agents/brain-researcher.md +47 -0
  9. package/agents/brain-synthesizer.md +43 -0
  10. package/agents/brain-verifier.md +41 -0
  11. package/bin/brain-tools.cjs +185 -0
  12. package/bin/lib/adr.cjs +283 -0
  13. package/bin/lib/agents.cjs +152 -0
  14. package/bin/lib/anti-patterns.cjs +183 -0
  15. package/bin/lib/audit.cjs +268 -0
  16. package/bin/lib/commands/adr.cjs +126 -0
  17. package/bin/lib/commands/complete.cjs +270 -0
  18. package/bin/lib/commands/config.cjs +306 -0
  19. package/bin/lib/commands/discuss.cjs +237 -0
  20. package/bin/lib/commands/execute.cjs +415 -0
  21. package/bin/lib/commands/health.cjs +103 -0
  22. package/bin/lib/commands/map.cjs +101 -0
  23. package/bin/lib/commands/new-project.cjs +885 -0
  24. package/bin/lib/commands/pause.cjs +142 -0
  25. package/bin/lib/commands/phase-manage.cjs +357 -0
  26. package/bin/lib/commands/plan.cjs +451 -0
  27. package/bin/lib/commands/progress.cjs +167 -0
  28. package/bin/lib/commands/quick.cjs +447 -0
  29. package/bin/lib/commands/resume.cjs +196 -0
  30. package/bin/lib/commands/storm.cjs +590 -0
  31. package/bin/lib/commands/verify.cjs +504 -0
  32. package/bin/lib/commands.cjs +263 -0
  33. package/bin/lib/complexity.cjs +138 -0
  34. package/bin/lib/complexity.test.cjs +108 -0
  35. package/bin/lib/config.cjs +452 -0
  36. package/bin/lib/core.cjs +62 -0
  37. package/bin/lib/detect.cjs +603 -0
  38. package/bin/lib/git.cjs +112 -0
  39. package/bin/lib/health.cjs +356 -0
  40. package/bin/lib/init.cjs +310 -0
  41. package/bin/lib/logger.cjs +100 -0
  42. package/bin/lib/platform.cjs +58 -0
  43. package/bin/lib/requirements.cjs +158 -0
  44. package/bin/lib/roadmap.cjs +228 -0
  45. package/bin/lib/security.cjs +237 -0
  46. package/bin/lib/state.cjs +353 -0
  47. package/bin/lib/templates.cjs +48 -0
  48. package/bin/templates/advocate.md +182 -0
  49. package/bin/templates/checkpoint.md +55 -0
  50. package/bin/templates/debugger.md +148 -0
  51. package/bin/templates/discuss.md +60 -0
  52. package/bin/templates/executor.md +201 -0
  53. package/bin/templates/mapper.md +129 -0
  54. package/bin/templates/plan-checker.md +134 -0
  55. package/bin/templates/planner.md +165 -0
  56. package/bin/templates/researcher.md +78 -0
  57. package/bin/templates/storm.html +376 -0
  58. package/bin/templates/synthesis.md +30 -0
  59. package/bin/templates/verifier.md +181 -0
  60. package/commands/brain/adr.md +34 -0
  61. package/commands/brain/complete.md +37 -0
  62. package/commands/brain/config.md +37 -0
  63. package/commands/brain/discuss.md +35 -0
  64. package/commands/brain/execute.md +38 -0
  65. package/commands/brain/health.md +33 -0
  66. package/commands/brain/map.md +35 -0
  67. package/commands/brain/new-project.md +38 -0
  68. package/commands/brain/pause.md +26 -0
  69. package/commands/brain/plan.md +38 -0
  70. package/commands/brain/progress.md +28 -0
  71. package/commands/brain/quick.md +51 -0
  72. package/commands/brain/resume.md +28 -0
  73. package/commands/brain/storm.md +30 -0
  74. package/commands/brain/verify.md +39 -0
  75. package/hooks/bootstrap.sh +54 -0
  76. package/hooks/post-tool-use.sh +45 -0
  77. package/hooks/statusline.sh +130 -0
  78. package/package.json +36 -0
@@ -0,0 +1,356 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { migrateState, atomicWriteSync } = require('./state.cjs');
6
+
7
+ /**
8
+ * Expected subdirectories under .brain/
9
+ */
10
+ const EXPECTED_SUBDIRS = ['hooks', 'debug', 'sessions', 'specs', 'codebase'];
11
+
12
+ /**
13
+ * Health check definitions.
14
+ * Each check has: name, category ('safe'|'report'), check(brainDir), repair(brainDir)|null
15
+ */
16
+ const CHECKS = [
17
+ // --- Safe (auto-repairable) checks ---
18
+ {
19
+ name: 'brain-subdirs',
20
+ category: 'safe',
21
+ check(brainDir) {
22
+ const missing = EXPECTED_SUBDIRS.filter(
23
+ s => !fs.existsSync(path.join(brainDir, s))
24
+ );
25
+ if (missing.length === 0) {
26
+ return { status: 'pass', message: 'All subdirectories present' };
27
+ }
28
+ return { status: 'fail', message: `Missing subdirs: ${missing.join(', ')}` };
29
+ },
30
+ repair(brainDir) {
31
+ for (const s of EXPECTED_SUBDIRS) {
32
+ const p = path.join(brainDir, s);
33
+ if (!fs.existsSync(p)) {
34
+ fs.mkdirSync(p, { recursive: true });
35
+ }
36
+ }
37
+ }
38
+ },
39
+ {
40
+ name: 'stale-bridge',
41
+ category: 'safe',
42
+ check(brainDir) {
43
+ const bridgePath = path.join(brainDir, '.context-bridge.json');
44
+ if (!fs.existsSync(bridgePath)) {
45
+ return { status: 'pass', message: 'No bridge file' };
46
+ }
47
+ // Read state to check session staleness
48
+ const statePath = path.join(brainDir, 'brain.json');
49
+ try {
50
+ const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
51
+ const lastPaused = state.session && state.session.lastPaused;
52
+ if (lastPaused) {
53
+ // Session is paused -- bridge may be stale
54
+ const pausedAt = new Date(lastPaused).getTime();
55
+ const now = Date.now();
56
+ if (now - pausedAt > 24 * 60 * 60 * 1000) {
57
+ return { status: 'fail', message: 'Bridge file stale (session paused >24h ago)' };
58
+ }
59
+ }
60
+ // Active session with no pause -- bridge is fine
61
+ if (!lastPaused) {
62
+ return { status: 'pass', message: 'Bridge file present, session active' };
63
+ }
64
+ return { status: 'pass', message: 'Bridge file present, session recent' };
65
+ } catch {
66
+ return { status: 'fail', message: 'Bridge file exists but cannot read state' };
67
+ }
68
+ },
69
+ repair(brainDir) {
70
+ const bridgePath = path.join(brainDir, '.context-bridge.json');
71
+ if (fs.existsSync(bridgePath)) {
72
+ fs.unlinkSync(bridgePath);
73
+ }
74
+ }
75
+ },
76
+ {
77
+ name: 'brain-json-fields',
78
+ category: 'safe',
79
+ check(brainDir) {
80
+ const statePath = path.join(brainDir, 'brain.json');
81
+ try {
82
+ const data = JSON.parse(fs.readFileSync(statePath, 'utf8'));
83
+ const migrated = migrateState(data);
84
+ // Compare stringified to detect any differences
85
+ if (JSON.stringify(data) !== JSON.stringify(migrated)) {
86
+ return { status: 'fail', message: 'brain.json missing fields (needs migration)' };
87
+ }
88
+ return { status: 'pass', message: 'brain.json fields up to date' };
89
+ } catch {
90
+ return { status: 'fail', message: 'Cannot parse brain.json for field check' };
91
+ }
92
+ },
93
+ repair(brainDir) {
94
+ const statePath = path.join(brainDir, 'brain.json');
95
+ try {
96
+ const data = JSON.parse(fs.readFileSync(statePath, 'utf8'));
97
+ const migrated = migrateState(data);
98
+ atomicWriteSync(statePath, JSON.stringify(migrated, null, 2));
99
+ } catch {
100
+ // Cannot repair if JSON is corrupt -- that's a report-only issue
101
+ }
102
+ }
103
+ },
104
+ {
105
+ name: 'orphaned-sessions',
106
+ category: 'safe',
107
+ check(brainDir) {
108
+ const sessDir = path.join(brainDir, 'sessions');
109
+ if (!fs.existsSync(sessDir)) {
110
+ return { status: 'pass', message: 'No sessions directory' };
111
+ }
112
+ const now = Date.now();
113
+ const sevenDays = 7 * 24 * 60 * 60 * 1000;
114
+ const files = fs.readdirSync(sessDir).filter(f => {
115
+ const fp = path.join(sessDir, f);
116
+ const stat = fs.statSync(fp);
117
+ return stat.isFile() && (now - stat.mtimeMs > sevenDays);
118
+ });
119
+ if (files.length === 0) {
120
+ return { status: 'pass', message: 'No orphaned sessions' };
121
+ }
122
+ return { status: 'fail', message: `${files.length} session(s) older than 7 days` };
123
+ },
124
+ repair(brainDir) {
125
+ const sessDir = path.join(brainDir, 'sessions');
126
+ if (!fs.existsSync(sessDir)) return;
127
+ const archiveDir = path.join(sessDir, 'archive');
128
+ fs.mkdirSync(archiveDir, { recursive: true });
129
+ const now = Date.now();
130
+ const sevenDays = 7 * 24 * 60 * 60 * 1000;
131
+ for (const f of fs.readdirSync(sessDir)) {
132
+ const fp = path.join(sessDir, f);
133
+ const stat = fs.statSync(fp);
134
+ if (stat.isFile() && (now - stat.mtimeMs > sevenDays)) {
135
+ fs.renameSync(fp, path.join(archiveDir, f));
136
+ }
137
+ }
138
+ }
139
+ },
140
+
141
+ // --- Report-only checks ---
142
+ {
143
+ name: 'brain-json-corrupt',
144
+ category: 'report',
145
+ check(brainDir) {
146
+ const statePath = path.join(brainDir, 'brain.json');
147
+ if (!fs.existsSync(statePath)) {
148
+ return { status: 'fail', message: 'brain.json does not exist' };
149
+ }
150
+ try {
151
+ JSON.parse(fs.readFileSync(statePath, 'utf8'));
152
+ return { status: 'pass', message: 'brain.json is valid JSON' };
153
+ } catch {
154
+ return { status: 'fail', message: 'brain.json is corrupt (invalid JSON)' };
155
+ }
156
+ },
157
+ repair: null
158
+ },
159
+ {
160
+ name: 'state-roadmap-mismatch',
161
+ category: 'report',
162
+ check(brainDir) {
163
+ const statePath = path.join(brainDir, 'brain.json');
164
+ // Find project root (parent of .brain/)
165
+ const projectRoot = path.dirname(brainDir);
166
+ const roadmapPath = path.join(projectRoot, '.planning', 'ROADMAP.md');
167
+
168
+ try {
169
+ const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
170
+ const currentPhase = state.phase && state.phase.current;
171
+
172
+ if (!fs.existsSync(roadmapPath)) {
173
+ if (currentPhase > 0) {
174
+ return { status: 'fail', message: 'State has active phase but no ROADMAP.md found' };
175
+ }
176
+ return { status: 'pass', message: 'No ROADMAP.md, state at phase 0' };
177
+ }
178
+
179
+ // Simple check: look for phase progress markers in ROADMAP.md
180
+ return { status: 'pass', message: 'State and roadmap present' };
181
+ } catch {
182
+ return { status: 'fail', message: 'Cannot read state for roadmap comparison' };
183
+ }
184
+ },
185
+ repair: null
186
+ },
187
+ {
188
+ name: 'unregistered-hooks',
189
+ category: 'report',
190
+ check(brainDir) {
191
+ const projectRoot = path.dirname(brainDir);
192
+ const settingsPath = path.join(projectRoot, '.claude', 'settings.json');
193
+ if (!fs.existsSync(settingsPath)) {
194
+ return { status: 'fail', message: 'No .claude/settings.json found' };
195
+ }
196
+ try {
197
+ const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
198
+ const hooks = settings.hooks || {};
199
+ const sessionStart = hooks.SessionStart || [];
200
+ const hasBootstrap = sessionStart.some(h => {
201
+ const cmd = typeof h === 'string' ? h : (h.command || '');
202
+ return cmd.includes('bootstrap.sh');
203
+ });
204
+ if (hasBootstrap) {
205
+ return { status: 'pass', message: 'SessionStart hook registered' };
206
+ }
207
+ return { status: 'fail', message: 'bootstrap.sh not registered in SessionStart hooks' };
208
+ } catch {
209
+ return { status: 'fail', message: 'Cannot parse .claude/settings.json' };
210
+ }
211
+ },
212
+ repair: null
213
+ },
214
+ {
215
+ name: 'missing-templates',
216
+ category: 'report',
217
+ check(brainDir) {
218
+ const templatesDir = path.join(__dirname, '..', 'templates');
219
+ const required = ['planner.md', 'executor.md', 'verifier.md', 'debugger.md'];
220
+ const missing = required.filter(t => !fs.existsSync(path.join(templatesDir, t)));
221
+ if (missing.length === 0) {
222
+ return { status: 'pass', message: 'All required templates present' };
223
+ }
224
+ return { status: 'fail', message: `Missing templates: ${missing.join(', ')}` };
225
+ },
226
+ repair: null
227
+ },
228
+ {
229
+ name: 'git-not-initialized',
230
+ category: 'report',
231
+ check(brainDir) {
232
+ const projectRoot = path.dirname(brainDir);
233
+ if (fs.existsSync(path.join(projectRoot, '.git'))) {
234
+ return { status: 'pass', message: 'Git repository initialized' };
235
+ }
236
+ return { status: 'fail', message: 'No .git/ directory found at project root' };
237
+ },
238
+ repair: null
239
+ }
240
+ ];
241
+
242
+ /**
243
+ * FIX_MODE_REPAIRS: aggressive repairs for --fix flag.
244
+ * Maps check name to repair function for report-category checks.
245
+ */
246
+ const FIX_MODE_REPAIRS = {
247
+ // Could add hook registration and template copying here in future
248
+ };
249
+
250
+ /**
251
+ * Run all health checks.
252
+ * @param {string} brainDir - Path to .brain/ directory
253
+ * @returns {Array<{name: string, status: string, category: string, message: string}>}
254
+ */
255
+ function runChecks(brainDir) {
256
+ return CHECKS.map(c => {
257
+ try {
258
+ const result = c.check(brainDir);
259
+ return {
260
+ name: c.name,
261
+ status: result.status,
262
+ category: c.category,
263
+ message: result.message
264
+ };
265
+ } catch (err) {
266
+ return {
267
+ name: c.name,
268
+ status: 'fail',
269
+ category: c.category,
270
+ message: `Check error: ${err.message}`
271
+ };
272
+ }
273
+ });
274
+ }
275
+
276
+ /**
277
+ * Auto-repair safe-category failures.
278
+ * @param {string} brainDir - Path to .brain/ directory
279
+ * @param {Array} results - Results from runChecks
280
+ * @returns {string[]} Names of repaired checks
281
+ */
282
+ function autoRepair(brainDir, results) {
283
+ const repaired = [];
284
+ for (const r of results) {
285
+ if (r.status === 'fail' && r.category === 'safe') {
286
+ const check = CHECKS.find(c => c.name === r.name);
287
+ if (check && check.repair) {
288
+ try {
289
+ check.repair(brainDir);
290
+ repaired.push(r.name);
291
+ r.status = 'repaired';
292
+ } catch {
293
+ // Repair failed, leave as fail
294
+ }
295
+ }
296
+ }
297
+ }
298
+ return repaired;
299
+ }
300
+
301
+ /**
302
+ * Generate a structured report from check results.
303
+ * @param {Array} results - Results from runChecks
304
+ * @returns {{total: number, passed: number, failed: number, checks: Array}}
305
+ */
306
+ function generateReport(results) {
307
+ const passed = results.filter(r => r.status === 'pass' || r.status === 'repaired').length;
308
+ const failed = results.filter(r => r.status === 'fail').length;
309
+ return {
310
+ total: results.length,
311
+ passed,
312
+ failed,
313
+ checks: results.map(r => ({
314
+ name: r.name,
315
+ status: r.status,
316
+ category: r.category,
317
+ message: r.message
318
+ }))
319
+ };
320
+ }
321
+
322
+ /**
323
+ * Quick health check: runs only safe-category checks, auto-repairs failures.
324
+ * Designed to complete in under 100ms for bootstrap use.
325
+ * @param {string} brainDir - Path to .brain/ directory
326
+ * @returns {boolean} true if all safe checks pass (or are repaired)
327
+ */
328
+ function quickCheck(brainDir) {
329
+ const safeChecks = CHECKS.filter(c => c.category === 'safe');
330
+ const results = safeChecks.map(c => {
331
+ try {
332
+ const result = c.check(brainDir);
333
+ return {
334
+ name: c.name,
335
+ status: result.status,
336
+ category: c.category,
337
+ message: result.message
338
+ };
339
+ } catch (err) {
340
+ return {
341
+ name: c.name,
342
+ status: 'fail',
343
+ category: c.category,
344
+ message: `Check error: ${err.message}`
345
+ };
346
+ }
347
+ });
348
+
349
+ // Auto-repair any failures
350
+ autoRepair(brainDir, results);
351
+
352
+ // All good if no remaining failures
353
+ return results.every(r => r.status !== 'fail');
354
+ }
355
+
356
+ module.exports = { CHECKS, FIX_MODE_REPAIRS, runChecks, autoRepair, generateReport, quickCheck };
@@ -0,0 +1,310 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { parseArgs } = require('node:util');
6
+ const { prefix, error } = require('./core.cjs');
7
+ const { createDefaultState, writeState, atomicWriteSync } = require('./state.cjs');
8
+ const { detectPlatform } = require('./platform.cjs');
9
+ const { isGitRepo, gitInit, gitCommit } = require('./git.cjs');
10
+
11
+ /**
12
+ * Resolve a path relative to the package root (two levels up from __dirname).
13
+ * @param {...string} segments - Path segments relative to package root
14
+ * @returns {string}
15
+ */
16
+ function packagePath(...segments) {
17
+ return path.join(__dirname, '..', '..', ...segments);
18
+ }
19
+
20
+ /**
21
+ * Print ASCII art banner for init command.
22
+ */
23
+ function showBanner() {
24
+ console.log('');
25
+ console.log(' ╔══════════════════════════════════════════╗');
26
+ console.log(' ║ ║');
27
+ console.log(' ║ ██████ ██████ █████ ██ ██ ██ ║');
28
+ console.log(' ║ ██ ██ ██ ██ ██ ██ ██ ███ ██ ║');
29
+ console.log(' ║ ██████ ██████ ███████ ██ ██ █ ██ ║');
30
+ console.log(' ║ ██ ██ ██ ██ ██ ██ ██ ██ ███ ║');
31
+ console.log(' ║ ██████ ██ ██ ██ ██ ██ ██ ██ ║');
32
+ console.log(' ║ ║');
33
+ console.log(' ║ AI workflow orchestrator ║');
34
+ console.log(' ║ ║');
35
+ console.log(' ║ ── halilcosdu ── ║');
36
+ console.log(' ║ ║');
37
+ console.log(' ╚══════════════════════════════════════════╝');
38
+ console.log('');
39
+ }
40
+
41
+ /**
42
+ * Merge brain SessionStart hook into .claude/settings.json.
43
+ * Preserves existing settings and hooks.
44
+ * @param {string} cwd - Working directory
45
+ */
46
+ function registerClaudeHooks(cwd) {
47
+ const claudeDir = path.join(cwd, '.claude');
48
+ const settingsPath = path.join(claudeDir, 'settings.json');
49
+
50
+ // Read existing or start fresh
51
+ let settings = {};
52
+ if (fs.existsSync(settingsPath)) {
53
+ try {
54
+ settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
55
+ } catch {
56
+ settings = {};
57
+ }
58
+ }
59
+
60
+ // Ensure hooks object exists
61
+ if (!settings.hooks) {
62
+ settings.hooks = {};
63
+ }
64
+
65
+ // Helper: create Claude Code hook entry in new format {matcher, hooks: [{type, command}]}
66
+ function makeHookEntry(command) {
67
+ return { matcher: '', hooks: [{ type: 'command', command }] };
68
+ }
69
+
70
+ // Helper: check if a hook command is already registered (handles both old and new format)
71
+ function isHookRegistered(hookArray, searchStr) {
72
+ if (!Array.isArray(hookArray)) return false;
73
+ return hookArray.some(entry => {
74
+ if (entry.command && entry.command.includes(searchStr)) return true;
75
+ if (entry.hooks) return entry.hooks.some(h => h.command && h.command.includes(searchStr));
76
+ return false;
77
+ });
78
+ }
79
+
80
+ // SessionStart hook
81
+ if (!Array.isArray(settings.hooks.SessionStart)) {
82
+ settings.hooks.SessionStart = [];
83
+ }
84
+ if (!isHookRegistered(settings.hooks.SessionStart, 'bootstrap.sh')) {
85
+ settings.hooks.SessionStart.push(
86
+ makeHookEntry('bash "$CLAUDE_PROJECT_DIR/.brain/hooks/bootstrap.sh"')
87
+ );
88
+ }
89
+
90
+ // StatusLine configuration (top-level, NOT under hooks)
91
+ if (!settings.statusLine) {
92
+ settings.statusLine = {
93
+ type: 'command',
94
+ command: 'bash "$CLAUDE_PROJECT_DIR/.brain/hooks/statusline.sh"'
95
+ };
96
+ }
97
+
98
+ // PostToolUse hook for context alerts
99
+ if (!Array.isArray(settings.hooks.PostToolUse)) {
100
+ settings.hooks.PostToolUse = [];
101
+ }
102
+ if (!isHookRegistered(settings.hooks.PostToolUse, 'post-tool-use.sh')) {
103
+ settings.hooks.PostToolUse.push(
104
+ makeHookEntry('bash "$CLAUDE_PROJECT_DIR/.brain/hooks/post-tool-use.sh"')
105
+ );
106
+ }
107
+
108
+ // Write settings
109
+ fs.mkdirSync(claudeDir, { recursive: true });
110
+ atomicWriteSync(settingsPath, JSON.stringify(settings, null, 2));
111
+ }
112
+
113
+ /**
114
+ * Clean up legacy skill directories from previous brain versions.
115
+ * Skills have been replaced by agent definitions + command markdown (GSD-style).
116
+ * @param {string} cwd - Working directory
117
+ */
118
+ function cleanupLegacySkills(cwd) {
119
+ // Only remove brain-created skill directories, NOT the entire .claude/skills/
120
+ const brainSkillNames = ['brain', 'tdd', 'debug', 'verify-complete', 'defense-in-depth', 'review-request', 'review-receive', 'brainstorm'];
121
+
122
+ const claudeSkillsDir = path.join(cwd, '.claude', 'skills');
123
+ if (fs.existsSync(claudeSkillsDir)) {
124
+ for (const name of brainSkillNames) {
125
+ const skillDir = path.join(claudeSkillsDir, name);
126
+ if (fs.existsSync(skillDir)) {
127
+ try { fs.rmSync(skillDir, { recursive: true, force: true }); } catch { /* ignore */ }
128
+ }
129
+ }
130
+ }
131
+
132
+ // .brain/skills/ is entirely brain-owned, safe to remove completely
133
+ const brainSkillsDir = path.join(cwd, '.brain', 'skills');
134
+ if (fs.existsSync(brainSkillsDir)) {
135
+ try { fs.rmSync(brainSkillsDir, { recursive: true, force: true }); } catch { /* ignore */ }
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Register brain agent definitions in .claude/agents/.
141
+ * Copies brain-* agent .md files so Claude Code uses brain-specific agents
142
+ * instead of falling back to globally installed alternatives (e.g. GSD).
143
+ * @param {string} cwd - Working directory
144
+ */
145
+ function registerAgents(cwd) {
146
+ const src = packagePath('agents');
147
+ if (!fs.existsSync(src)) {
148
+ return;
149
+ }
150
+ const dest = path.join(cwd, '.claude', 'agents');
151
+ fs.mkdirSync(dest, { recursive: true });
152
+ for (const file of fs.readdirSync(src)) {
153
+ if (file.startsWith('brain-') && file.endsWith('.md')) {
154
+ fs.copyFileSync(path.join(src, file), path.join(dest, file));
155
+ }
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Register command files from commands/brain/ into .claude/commands/brain/.
161
+ * @param {string} cwd - Working directory
162
+ */
163
+ function registerCommands(cwd) {
164
+ const src = packagePath('commands', 'brain');
165
+ if (!fs.existsSync(src)) {
166
+ return;
167
+ }
168
+ const dest = path.join(cwd, '.claude', 'commands', 'brain');
169
+ try {
170
+ fs.mkdirSync(dest, { recursive: true });
171
+ fs.cpSync(src, dest, { recursive: true });
172
+ } catch { /* skip on permission/copy failure */ }
173
+ }
174
+
175
+ /**
176
+ * Run the init command.
177
+ * @param {string[]} args - CLI arguments after 'init'
178
+ */
179
+ async function run(args = []) {
180
+ const { values } = parseArgs({
181
+ args,
182
+ options: {
183
+ force: { type: 'boolean', default: false }
184
+ },
185
+ strict: false
186
+ });
187
+
188
+ const cwd = process.cwd();
189
+ const brainDir = path.join(cwd, '.brain');
190
+
191
+ // Pre-flight: check existing .brain/
192
+ if (fs.existsSync(brainDir)) {
193
+ if (!values.force) {
194
+ error("'.brain/' already exists. Use --force to wipe and recreate.");
195
+ process.exit(1);
196
+ return;
197
+ }
198
+ // --force: remove existing
199
+ fs.rmSync(brainDir, { recursive: true, force: true });
200
+ }
201
+
202
+ // Show banner
203
+ showBanner();
204
+
205
+ // Detect platform
206
+ const platform = detectPlatform({ cwd });
207
+ const isClaudeCode = platform === 'claude-code';
208
+
209
+ // Create .brain/ skeleton
210
+ fs.mkdirSync(path.join(brainDir, 'hooks'), { recursive: true });
211
+ fs.mkdirSync(path.join(brainDir, 'debug'), { recursive: true });
212
+ fs.mkdirSync(path.join(brainDir, 'specs'), { recursive: true });
213
+
214
+ // Create default state and write brain.json + STATE.md
215
+ const state = createDefaultState(platform);
216
+
217
+ // Merge global defaults from ~/.brain/defaults.json if it exists
218
+ try {
219
+ const { mergeWithDefaults, deepMerge } = require('./config.cjs');
220
+ const os = require('node:os');
221
+ const globalDefaultsPath = path.join(os.homedir(), '.brain', 'defaults.json');
222
+ if (fs.existsSync(globalDefaultsPath)) {
223
+ const globalDefaults = JSON.parse(fs.readFileSync(globalDefaultsPath, 'utf8'));
224
+ // Apply global defaults onto state (state fields override globals)
225
+ const merged = deepMerge(state, globalDefaults);
226
+ Object.assign(state, merged);
227
+ }
228
+ } catch {
229
+ // Global defaults merge failed -- continue with default state
230
+ }
231
+
232
+ writeState(brainDir, state);
233
+
234
+ // Copy bootstrap.sh hook
235
+ const hookSrc = packagePath('hooks', 'bootstrap.sh');
236
+ const hookDest = path.join(brainDir, 'hooks', 'bootstrap.sh');
237
+ fs.copyFileSync(hookSrc, hookDest);
238
+ fs.chmodSync(hookDest, 0o755);
239
+
240
+ // Copy monitoring hooks
241
+ const statuslineSrc = packagePath('hooks', 'statusline.sh');
242
+ const postToolSrc = packagePath('hooks', 'post-tool-use.sh');
243
+ if (fs.existsSync(statuslineSrc)) {
244
+ fs.copyFileSync(statuslineSrc, path.join(brainDir, 'hooks', 'statusline.sh'));
245
+ fs.chmodSync(path.join(brainDir, 'hooks', 'statusline.sh'), 0o755);
246
+ }
247
+ if (fs.existsSync(postToolSrc)) {
248
+ fs.copyFileSync(postToolSrc, path.join(brainDir, 'hooks', 'post-tool-use.sh'));
249
+ fs.chmodSync(path.join(brainDir, 'hooks', 'post-tool-use.sh'), 0o755);
250
+ }
251
+
252
+ // Write .gitignore
253
+ const gitignoreContent = [
254
+ '*.tmp',
255
+ '*.lock',
256
+ 'storm/fragments/',
257
+ 'storm/events.jsonl',
258
+ ''
259
+ ].join('\n');
260
+ fs.writeFileSync(path.join(brainDir, '.gitignore'), gitignoreContent, 'utf8');
261
+
262
+ // Output lines
263
+ console.log(prefix('Created .brain/ directory'));
264
+ console.log(prefix(`Platform: ${platform}${!isClaudeCode ? ' (stub -- full support coming in v2)' : ''}`));
265
+
266
+ // Platform-specific registrations
267
+ if (isClaudeCode) {
268
+ registerClaudeHooks(cwd);
269
+ console.log(prefix('Registered SessionStart hook'));
270
+
271
+ cleanupLegacySkills(cwd);
272
+
273
+ registerCommands(cwd);
274
+ console.log(prefix('Registered brain commands'));
275
+
276
+ registerAgents(cwd);
277
+ console.log(prefix('Registered brain agents'));
278
+ } else {
279
+ console.log(prefix('Note: brain currently supports Claude Code only. Other platform adapters are planned for v2.'));
280
+ }
281
+
282
+ // Git integration
283
+ const filesToCommit = [
284
+ '.brain/brain.json',
285
+ '.brain/STATE.md',
286
+ '.brain/hooks/bootstrap.sh',
287
+ '.brain/.gitignore'
288
+ ];
289
+
290
+ if (isClaudeCode) {
291
+ filesToCommit.push('.claude/settings.json');
292
+ filesToCommit.push('.claude/commands/brain/');
293
+ filesToCommit.push('.claude/agents/');
294
+ }
295
+
296
+ if (!isGitRepo(cwd)) {
297
+ gitInit(cwd);
298
+ console.log(prefix('Initialized git repository'));
299
+ }
300
+
301
+ const committed = gitCommit('chore: initialize brain', filesToCommit, cwd);
302
+ if (committed) {
303
+ console.log(prefix('Committed: chore: initialize brain'));
304
+ }
305
+
306
+ console.log(prefix(''));
307
+ console.log(prefix('Next: /brain:new-project to start planning'));
308
+ }
309
+
310
+ module.exports = { run };