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.
- package/LICENSE +21 -0
- package/README.md +152 -0
- package/agents/brain-checker.md +33 -0
- package/agents/brain-debugger.md +35 -0
- package/agents/brain-executor.md +37 -0
- package/agents/brain-mapper.md +44 -0
- package/agents/brain-planner.md +49 -0
- package/agents/brain-researcher.md +47 -0
- package/agents/brain-synthesizer.md +43 -0
- package/agents/brain-verifier.md +41 -0
- package/bin/brain-tools.cjs +185 -0
- package/bin/lib/adr.cjs +283 -0
- package/bin/lib/agents.cjs +152 -0
- package/bin/lib/anti-patterns.cjs +183 -0
- package/bin/lib/audit.cjs +268 -0
- package/bin/lib/commands/adr.cjs +126 -0
- package/bin/lib/commands/complete.cjs +270 -0
- package/bin/lib/commands/config.cjs +306 -0
- package/bin/lib/commands/discuss.cjs +237 -0
- package/bin/lib/commands/execute.cjs +415 -0
- package/bin/lib/commands/health.cjs +103 -0
- package/bin/lib/commands/map.cjs +101 -0
- package/bin/lib/commands/new-project.cjs +885 -0
- package/bin/lib/commands/pause.cjs +142 -0
- package/bin/lib/commands/phase-manage.cjs +357 -0
- package/bin/lib/commands/plan.cjs +451 -0
- package/bin/lib/commands/progress.cjs +167 -0
- package/bin/lib/commands/quick.cjs +447 -0
- package/bin/lib/commands/resume.cjs +196 -0
- package/bin/lib/commands/storm.cjs +590 -0
- package/bin/lib/commands/verify.cjs +504 -0
- package/bin/lib/commands.cjs +263 -0
- package/bin/lib/complexity.cjs +138 -0
- package/bin/lib/complexity.test.cjs +108 -0
- package/bin/lib/config.cjs +452 -0
- package/bin/lib/core.cjs +62 -0
- package/bin/lib/detect.cjs +603 -0
- package/bin/lib/git.cjs +112 -0
- package/bin/lib/health.cjs +356 -0
- package/bin/lib/init.cjs +310 -0
- package/bin/lib/logger.cjs +100 -0
- package/bin/lib/platform.cjs +58 -0
- package/bin/lib/requirements.cjs +158 -0
- package/bin/lib/roadmap.cjs +228 -0
- package/bin/lib/security.cjs +237 -0
- package/bin/lib/state.cjs +353 -0
- package/bin/lib/templates.cjs +48 -0
- package/bin/templates/advocate.md +182 -0
- package/bin/templates/checkpoint.md +55 -0
- package/bin/templates/debugger.md +148 -0
- package/bin/templates/discuss.md +60 -0
- package/bin/templates/executor.md +201 -0
- package/bin/templates/mapper.md +129 -0
- package/bin/templates/plan-checker.md +134 -0
- package/bin/templates/planner.md +165 -0
- package/bin/templates/researcher.md +78 -0
- package/bin/templates/storm.html +376 -0
- package/bin/templates/synthesis.md +30 -0
- package/bin/templates/verifier.md +181 -0
- package/commands/brain/adr.md +34 -0
- package/commands/brain/complete.md +37 -0
- package/commands/brain/config.md +37 -0
- package/commands/brain/discuss.md +35 -0
- package/commands/brain/execute.md +38 -0
- package/commands/brain/health.md +33 -0
- package/commands/brain/map.md +35 -0
- package/commands/brain/new-project.md +38 -0
- package/commands/brain/pause.md +26 -0
- package/commands/brain/plan.md +38 -0
- package/commands/brain/progress.md +28 -0
- package/commands/brain/quick.md +51 -0
- package/commands/brain/resume.md +28 -0
- package/commands/brain/storm.md +30 -0
- package/commands/brain/verify.md +39 -0
- package/hooks/bootstrap.sh +54 -0
- package/hooks/post-tool-use.sh +45 -0
- package/hooks/statusline.sh +130 -0
- 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 };
|
package/bin/lib/init.cjs
ADDED
|
@@ -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 };
|