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,415 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { readState, writeState } = require('../state.cjs');
6
+ const { loadTemplate, interpolate } = require('../templates.cjs');
7
+ const { getAgent, resolveModel } = require('../agents.cjs');
8
+ const { logEvent } = require('../logger.cjs');
9
+ const { output, error } = require('../core.cjs');
10
+
11
+ /**
12
+ * Find a phase directory under .brain/phases/ matching a phase number.
13
+ * @param {string} brainDir
14
+ * @param {number} phaseNumber
15
+ * @returns {string|null}
16
+ */
17
+ function findPhaseDir(brainDir, phaseNumber) {
18
+ const phasesDir = path.join(brainDir, 'phases');
19
+ if (!fs.existsSync(phasesDir)) return null;
20
+
21
+ const padded = String(phaseNumber).padStart(2, '0');
22
+ const match = fs.readdirSync(phasesDir).find(d => d.startsWith(`${padded}-`));
23
+ return match ? path.join(phasesDir, match) : null;
24
+ }
25
+
26
+ /**
27
+ * Scan a phase directory for PLAN-*.md files and return them sorted.
28
+ * @param {string} phaseDir
29
+ * @returns {Array<{num: number, path: string, hasSummary: boolean}>}
30
+ */
31
+ function scanPlans(phaseDir) {
32
+ if (!phaseDir || !fs.existsSync(phaseDir)) return [];
33
+
34
+ const files = fs.readdirSync(phaseDir);
35
+ const plans = [];
36
+
37
+ for (const f of files) {
38
+ const match = f.match(/^PLAN-(\d+)\.md$/);
39
+ if (match) {
40
+ const num = parseInt(match[1], 10);
41
+ const padded = String(num).padStart(2, '0');
42
+ const hasSummary = files.includes(`SUMMARY-${padded}.md`);
43
+ plans.push({
44
+ num,
45
+ path: path.join(phaseDir, f),
46
+ hasSummary
47
+ });
48
+ }
49
+ }
50
+
51
+ plans.sort((a, b) => a.num - b.num);
52
+ return plans;
53
+ }
54
+
55
+ /**
56
+ * Extract task names from plan content.
57
+ * @param {string} content - Plan file content
58
+ * @returns {string[]}
59
+ */
60
+ function extractTasks(content) {
61
+ const tasks = [];
62
+ const taskRegex = /<name>(.*?)<\/name>/g;
63
+ let match;
64
+ while ((match = taskRegex.exec(content)) !== null) {
65
+ tasks.push(match[1].trim());
66
+ }
67
+ return tasks;
68
+ }
69
+
70
+ /**
71
+ * Spot-check a completed plan by verifying SUMMARY.md content.
72
+ * Checks key files exist and Self-Check marker is present.
73
+ *
74
+ * @param {string} phaseDir - Phase directory path
75
+ * @param {number} planNum - Plan number
76
+ * @param {string} summaryPath - Path to SUMMARY.md
77
+ * @returns {{ passed: boolean, checks: Array<{check: string, file?: string, passed: boolean}> }}
78
+ */
79
+ function spotCheckPlan(phaseDir, planNum, summaryPath) {
80
+ const checks = [];
81
+
82
+ // Check SUMMARY.md exists
83
+ if (!fs.existsSync(summaryPath)) {
84
+ checks.push({ check: 'summary-exists', file: summaryPath, passed: false });
85
+ return { passed: false, checks };
86
+ }
87
+
88
+ const content = fs.readFileSync(summaryPath, 'utf8');
89
+ checks.push({ check: 'summary-exists', file: summaryPath, passed: true });
90
+
91
+ // Check summary is non-empty
92
+ const isNonEmpty = content.trim().length > 0;
93
+ checks.push({ check: 'summary-non-empty', file: summaryPath, passed: isNonEmpty });
94
+
95
+ // Parse "Key Files" section and verify each file exists
96
+ const keyFilesMatch = content.match(/## Key Files[\s\S]*?(?=\n## |\n---|\Z)/);
97
+ if (keyFilesMatch) {
98
+ const filePathRegex = /[-*]\s+`?([^\s`]+\.\w+)`?/g;
99
+ let fileMatch;
100
+ while ((fileMatch = filePathRegex.exec(keyFilesMatch[0])) !== null) {
101
+ const filePath = fileMatch[1];
102
+ // Only check paths that look like real file paths (not headers or descriptions)
103
+ if (filePath.includes('/') || filePath.includes('.')) {
104
+ const projectRoot = path.dirname(path.dirname(phaseDir));
105
+ const fullPath = path.isAbsolute(filePath) ? filePath : path.resolve(projectRoot, filePath);
106
+ // Guard against path traversal: resolved path must be within project root
107
+ if (!fullPath.startsWith(projectRoot + path.sep) && fullPath !== projectRoot) {
108
+ checks.push({ check: 'key-file-exists', file: filePath, passed: false });
109
+ continue;
110
+ }
111
+ const exists = fs.existsSync(fullPath);
112
+ checks.push({ check: 'key-file-exists', file: filePath, passed: exists });
113
+ }
114
+ }
115
+ }
116
+
117
+ // Check for Self-Check: PASSED marker
118
+ const hasSelfCheck = content.includes('Self-Check: PASSED');
119
+ checks.push({ check: 'self-check-marker', passed: hasSelfCheck });
120
+
121
+ const passed = checks.every(c => c.passed);
122
+ return { passed, checks };
123
+ }
124
+
125
+ /**
126
+ * Build failure handling instructions for the executor prompt.
127
+ * Describes auto-fix scope, escalation scope, retry behavior, and debugger spawning.
128
+ *
129
+ * @returns {string} Markdown text block
130
+ */
131
+ function buildFailureInstructions() {
132
+ return [
133
+ '## Failure Handling',
134
+ '',
135
+ '### Auto-fix Scope (retry automatically)',
136
+ '- Test failures in code you wrote',
137
+ '- Import errors and missing module references',
138
+ '- Type mismatches and incorrect function signatures',
139
+ '- Missing files that should have been created',
140
+ '- Lint and formatting issues',
141
+ '',
142
+ '### Escalate Scope (do NOT retry, output EXECUTION FAILED)',
143
+ '- API contract changes that affect other plans',
144
+ '- New dependencies not in the plan',
145
+ '- Database schema changes',
146
+ '- Architectural deviations from plan',
147
+ '- Changes to shared interfaces',
148
+ '',
149
+ '### Retry Behavior',
150
+ '- On failure from auto-fix scope: retry once automatically',
151
+ '- If auto-retry fails: output EXECUTION FAILED with error context',
152
+ '- The orchestrator will then spawn a debugger agent with the error context',
153
+ '',
154
+ '### Debugger Path',
155
+ '- Debug sessions are stored at: `.brain/debug/issue-{task-slug}.md`',
156
+ '- If a debugger agent is spawned, it will receive the error context, task context, and attempted fixes'
157
+ ].join('\n');
158
+ }
159
+
160
+ /**
161
+ * Build debugger spawn instructions by interpolating the debugger template with runtime values.
162
+ * Called by the orchestrator when auto-retry fails and a debugger agent must be spawned.
163
+ *
164
+ * @param {string} taskSlug - Slug for the failed task
165
+ * @param {string} errorContext - Error message and stack trace
166
+ * @param {string} taskContext - Description of what the task was doing
167
+ * @param {string} attemptedFixes - What was already tried
168
+ * @param {object} [state] - brain.json state for model resolution
169
+ * @returns {{ action: string, prompt: string, model: string, session_path: string }}
170
+ */
171
+ function buildDebuggerSpawnInstructions(taskSlug, errorContext, taskContext, attemptedFixes, state) {
172
+ const tpl = loadTemplate('debugger');
173
+ const sessionPath = '.brain/debug/issue-' + taskSlug + '.md';
174
+
175
+ const interpolatedPrompt = interpolate(tpl, {
176
+ error_context: errorContext,
177
+ task_context: taskContext,
178
+ attempted_fixes: attemptedFixes,
179
+ debug_session_path: sessionPath
180
+ });
181
+
182
+ return {
183
+ action: 'spawn-debugger',
184
+ prompt: interpolatedPrompt,
185
+ model: resolveModel('debugger', state || null),
186
+ session_path: sessionPath
187
+ };
188
+ }
189
+
190
+ /**
191
+ * Build checkpoint protocol instructions for the executor prompt.
192
+ * Describes how to handle non-autonomous plans with checkpoint gates.
193
+ *
194
+ * @returns {string} Markdown text block
195
+ */
196
+ function buildCheckpointInstructions() {
197
+ return [
198
+ '## Checkpoint Protocol',
199
+ '',
200
+ 'For non-autonomous plans, checkpoints pause execution for human input.',
201
+ '',
202
+ '### checkpoint:human-verify',
203
+ 'After completing automated work, present what was built for visual/functional verification.',
204
+ 'Output a structured checkpoint block:',
205
+ '```',
206
+ '## CHECKPOINT REACHED',
207
+ 'Type: human-verify',
208
+ 'Progress: {completed}/{total} tasks',
209
+ 'What was built: [description]',
210
+ 'Verification steps: [URLs, commands, expected behavior]',
211
+ '```',
212
+ '',
213
+ '### checkpoint:decision',
214
+ 'When implementation requires a choice between options.',
215
+ 'Output a structured checkpoint block with options table.',
216
+ '',
217
+ '### checkpoint:human-action',
218
+ 'When a truly unavoidable manual step is needed (email link, 2FA code).',
219
+ 'Output what automation was attempted and the single manual step needed.'
220
+ ].join('\n');
221
+ }
222
+
223
+ /**
224
+ * Run the execute command.
225
+ *
226
+ * Default: Find the next unexecuted plan in current phase and output executor instructions.
227
+ * --plan NN: Target a specific plan.
228
+ * --phase N: Target a specific phase.
229
+ * --spot-check NN: Run spot-check verification for plan NN.
230
+ *
231
+ * @param {string[]} args - CLI arguments
232
+ * @param {object} [opts] - Options (brainDir for testing)
233
+ * @returns {object} Structured result
234
+ */
235
+ async function run(args = [], opts = {}) {
236
+ const brainDir = opts.brainDir || path.join(process.cwd(), '.brain');
237
+ const state = readState(brainDir);
238
+
239
+ if (!state) {
240
+ error("No brain state found. Run 'brain-dev init' first.");
241
+ return { error: 'no-state' };
242
+ }
243
+
244
+ // Determine phase
245
+ const phaseIdx = args.indexOf('--phase');
246
+ const phaseNumber = phaseIdx >= 0
247
+ ? parseInt(args[phaseIdx + 1], 10)
248
+ : state.phase.current;
249
+
250
+ // Handle --spot-check flag
251
+ const spotCheckIdx = args.indexOf('--spot-check');
252
+ if (spotCheckIdx >= 0) {
253
+ const planNum = parseInt(args[spotCheckIdx + 1], 10);
254
+ const phaseDir = findPhaseDir(brainDir, phaseNumber);
255
+ if (!phaseDir) {
256
+ error(`Phase ${phaseNumber} directory not found.`);
257
+ return { error: 'phase-not-found' };
258
+ }
259
+ const padded = String(planNum).padStart(2, '0');
260
+ const summaryPath = path.join(phaseDir, `SUMMARY-${padded}.md`);
261
+ const checkResult = spotCheckPlan(phaseDir, planNum, summaryPath);
262
+
263
+ logEvent(brainDir, phaseNumber, {
264
+ type: 'spot-check',
265
+ plan: padded,
266
+ passed: checkResult.passed,
267
+ checks: checkResult.checks.length
268
+ });
269
+
270
+ const msg = checkResult.passed
271
+ ? `Spot-check PASSED for plan ${padded}`
272
+ : `Spot-check FAILED for plan ${padded}`;
273
+ output({ action: 'spot-check', ...checkResult }, `[brain] ${msg}`);
274
+ return { action: 'spot-check', ...checkResult };
275
+ }
276
+
277
+ // Find phase directory
278
+ const phaseDir = findPhaseDir(brainDir, phaseNumber);
279
+ const plans = scanPlans(phaseDir);
280
+
281
+ if (plans.length === 0) {
282
+ error(`No plans found for phase ${phaseNumber}. Run '/brain:plan' first.`);
283
+ return { error: 'no-plans' };
284
+ }
285
+
286
+ // Determine which plan to execute
287
+ const planFlag = args.indexOf('--plan');
288
+ let targetPlan;
289
+
290
+ if (planFlag >= 0) {
291
+ const planNum = parseInt(args[planFlag + 1], 10);
292
+ targetPlan = plans.find(p => p.num === planNum);
293
+ if (!targetPlan) {
294
+ error(`Plan ${planNum} not found in phase ${phaseNumber}.`);
295
+ return { error: 'plan-not-found' };
296
+ }
297
+ } else {
298
+ // Find next unexecuted plan (no SUMMARY)
299
+ targetPlan = plans.find(p => !p.hasSummary);
300
+ }
301
+
302
+ // All plans executed
303
+ if (!targetPlan) {
304
+ state.phase.status = 'executed';
305
+ writeState(brainDir, state);
306
+ const msg = "All plans executed. Run /brain:verify to check the work.";
307
+ output({ action: 'all-executed', message: msg, nextAction: '/brain:verify' }, `[brain] ${msg}`);
308
+ return { action: 'all-executed', message: msg, nextAction: '/brain:verify' };
309
+ }
310
+
311
+ // Read plan content
312
+ const planContent = fs.readFileSync(targetPlan.path, 'utf8');
313
+ const tasks = extractTasks(planContent);
314
+
315
+ // Build summary path
316
+ const padded = String(targetPlan.num).padStart(2, '0');
317
+ const summaryPath = path.join(phaseDir, `SUMMARY-${padded}.md`);
318
+
319
+ // Extract phase/plan info from plan frontmatter
320
+ const phaseMatch = planContent.match(/^phase:\s*(.+)$/m);
321
+ const planNumMatch = planContent.match(/^plan:\s*(.+)$/m);
322
+
323
+ // Check autonomous flag from frontmatter
324
+ const autonomousMatch = planContent.match(/^autonomous:\s*(true|false)/m);
325
+ const autonomous = autonomousMatch ? autonomousMatch[1] === 'true' : true;
326
+
327
+ // Get executor agent metadata and resolve model
328
+ const executorAgent = getAgent('executor');
329
+ const model = resolveModel('executor', state);
330
+
331
+ // Log spawn event
332
+ logEvent(brainDir, phaseNumber, {
333
+ type: 'spawn',
334
+ agent: 'executor',
335
+ plan: padded,
336
+ autonomous,
337
+ model
338
+ });
339
+
340
+ // Build failure and checkpoint instructions
341
+ const failureInstructions = buildFailureInstructions();
342
+ const checkpointInstructions = autonomous ? '' : buildCheckpointInstructions();
343
+
344
+ const spotCheckInstruction = 'After completing all tasks, the orchestrator will verify SUMMARY.md, file existence, and commit count.';
345
+ const debuggerSpawnInstruction = 'If auto-retry fails, a debugger agent will be spawned with the error context.';
346
+
347
+ // Load executor template and interpolate
348
+ const template = loadTemplate('executor');
349
+ const prompt = interpolate(template, {
350
+ plan_path: targetPlan.path,
351
+ summary_path: summaryPath,
352
+ plan_content: planContent,
353
+ phase: phaseMatch ? phaseMatch[1].trim() : String(phaseNumber),
354
+ plan_number: planNumMatch ? planNumMatch[1].trim() : String(targetPlan.num),
355
+ subsystem: phaseMatch ? phaseMatch[1].trim() : String(phaseNumber)
356
+ });
357
+
358
+ // Append orchestration instructions to prompt
359
+ const fullPrompt = [
360
+ prompt,
361
+ '',
362
+ failureInstructions,
363
+ checkpointInstructions ? '\n' + checkpointInstructions : '',
364
+ '',
365
+ `> ${spotCheckInstruction}`,
366
+ `> ${debuggerSpawnInstruction}`
367
+ ].join('\n');
368
+
369
+ // Update state to executing
370
+ state.phase.status = 'executing';
371
+ writeState(brainDir, state);
372
+
373
+ // Check auto-recover config
374
+ const autoRecover = state.workflow?.auto_recover === true;
375
+
376
+ const result = {
377
+ action: 'execute-plan',
378
+ plan_path: targetPlan.path,
379
+ summary_path: summaryPath,
380
+ tasks,
381
+ prompt: fullPrompt,
382
+ model,
383
+ autonomous,
384
+ spot_check: true,
385
+ failure_handling: 'retry-then-debugger',
386
+ auto_recover: autoRecover,
387
+ debugger_spawn: (taskSlug, errorCtx, taskCtx, attemptedFixes) =>
388
+ buildDebuggerSpawnInstructions(taskSlug, errorCtx, taskCtx, attemptedFixes, state)
389
+ };
390
+
391
+ // Build verify command with optional --auto-recover
392
+ const verifyCmd = autoRecover
393
+ ? `brain-dev verify --phase ${phaseNumber} --auto-recover`
394
+ : `brain-dev verify --phase ${phaseNumber}`;
395
+
396
+ const humanLines = [
397
+ `[brain] Executor instructions generated for Plan ${padded} in Phase ${phaseNumber}`,
398
+ `[brain] Plan: ${targetPlan.path}`,
399
+ `[brain] Summary output: ${summaryPath}`,
400
+ `[brain] Tasks: ${tasks.length}`,
401
+ `[brain] Model: ${model}`,
402
+ `[brain] Autonomous: ${autonomous}`,
403
+ ];
404
+ if (autoRecover) {
405
+ humanLines.push('[brain] Auto-recovery: enabled (verify will auto-diagnose failures)');
406
+ }
407
+ humanLines.push(`[brain] Verify with: ${verifyCmd}`);
408
+ humanLines.push('');
409
+ humanLines.push(fullPrompt);
410
+ output(result, humanLines.join('\n'));
411
+
412
+ return result;
413
+ }
414
+
415
+ module.exports = { run, spotCheckPlan, buildDebuggerSpawnInstructions, buildFailureInstructions, buildCheckpointInstructions };
@@ -0,0 +1,103 @@
1
+ 'use strict';
2
+
3
+ const path = require('node:path');
4
+ const { runChecks, autoRepair, generateReport, quickCheck, FIX_MODE_REPAIRS } = require('../health.cjs');
5
+ const { output, error, success } = require('../core.cjs');
6
+
7
+ /**
8
+ * Health command handler.
9
+ * Runs health diagnostics and auto-repairs safe issues.
10
+ *
11
+ * Flags:
12
+ * --fix Enable aggressive repair (FIX_MODE_REPAIRS)
13
+ * --quick Run quickCheck only (for bootstrap)
14
+ * --json Force JSON output
15
+ *
16
+ * @param {string[]} args - CLI arguments
17
+ * @param {object} [opts] - Options (brainDir override)
18
+ * @returns {object} Result object
19
+ */
20
+ async function run(args = [], opts = {}) {
21
+ const brainDir = opts.brainDir || path.join(process.cwd(), '.brain');
22
+ const fix = args.includes('--fix');
23
+ const quick = args.includes('--quick');
24
+ const json = args.includes('--json');
25
+
26
+ // Quick mode: run safe checks only, output pass/fail
27
+ if (quick) {
28
+ const ok = quickCheck(brainDir);
29
+ const result = { action: 'health-quick', passed: ok };
30
+ if (json) {
31
+ console.log(JSON.stringify(result));
32
+ } else {
33
+ if (ok) {
34
+ success('Health: all safe checks passed');
35
+ } else {
36
+ error('Health: some safe checks failed (auto-repaired)');
37
+ }
38
+ }
39
+ return result;
40
+ }
41
+
42
+ // Full mode
43
+ const results = runChecks(brainDir);
44
+
45
+ // Auto-repair safe-category failures
46
+ const repaired = autoRepair(brainDir, results);
47
+
48
+ // Aggressive fix mode: run FIX_MODE_REPAIRS for fixable report-category failures
49
+ const fixRepaired = [];
50
+ if (fix) {
51
+ for (const r of results) {
52
+ if (r.status === 'fail' && r.category === 'report' && FIX_MODE_REPAIRS[r.name]) {
53
+ try {
54
+ FIX_MODE_REPAIRS[r.name](brainDir);
55
+ fixRepaired.push(r.name);
56
+ r.status = 'repaired';
57
+ } catch (e) {
58
+ r.fixError = e.message;
59
+ }
60
+ }
61
+ }
62
+ }
63
+
64
+ const report = generateReport(results);
65
+ const allPassed = report.failed === 0;
66
+
67
+ const resultObj = {
68
+ action: 'health-check',
69
+ passed: allPassed,
70
+ results: report,
71
+ repaired: [...repaired, ...fixRepaired]
72
+ };
73
+
74
+ if (json) {
75
+ console.log(JSON.stringify(resultObj, null, 2));
76
+ } else {
77
+ // Human-readable table
78
+ const lines = ['', '[brain] Health Check Results', ''];
79
+ const nameWidth = Math.max(...report.checks.map(c => c.name.length), 4);
80
+ lines.push(` ${'Check'.padEnd(nameWidth)} Status Category Message`);
81
+ lines.push(` ${''.padEnd(nameWidth, '-')} -------- -------- -------`);
82
+ for (const c of report.checks) {
83
+ const statusIcon = c.status === 'pass' ? 'PASS' :
84
+ c.status === 'repaired' ? 'FIXED' : 'FAIL';
85
+ lines.push(` ${c.name.padEnd(nameWidth)} ${statusIcon.padEnd(8)} ${c.category.padEnd(8)} ${c.message}`);
86
+ }
87
+ lines.push('');
88
+ lines.push(` Total: ${report.total} Passed: ${report.passed} Failed: ${report.failed}`);
89
+ if (repaired.length > 0) {
90
+ lines.push(` Auto-repaired: ${repaired.join(', ')}`);
91
+ }
92
+ if (fixRepaired.length > 0) {
93
+ lines.push(` Fix-repaired: ${fixRepaired.join(', ')}`);
94
+ }
95
+ lines.push('');
96
+
97
+ output(resultObj, lines.join('\n'));
98
+ }
99
+
100
+ return resultObj;
101
+ }
102
+
103
+ module.exports = { run };
@@ -0,0 +1,101 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { getAgent, resolveModel } = require('../agents.cjs');
6
+ const { loadTemplate, interpolate } = require('../templates.cjs');
7
+ const { readState } = require('../state.cjs');
8
+ const { scanFiles } = require('../security.cjs');
9
+
10
+ /**
11
+ * Focus area to output file mapping.
12
+ * Each mapper focus produces specific structured documents.
13
+ */
14
+ const FOCUS_OUTPUT_MAP = {
15
+ tech: ['STACK.md', 'INTEGRATIONS.md'],
16
+ arch: ['ARCHITECTURE.md', 'STRUCTURE.md'],
17
+ quality: ['CONVENTIONS.md', 'TESTING.md'],
18
+ concerns: ['CONCERNS.md']
19
+ };
20
+
21
+ /**
22
+ * Run the /brain:map command.
23
+ * Generates mapper agent spawn instructions for codebase analysis.
24
+ *
25
+ * @param {string[]} args - CLI arguments
26
+ * @param {object} [opts] - Options
27
+ * @param {string} [opts.brainDir] - Path to .brain/ directory
28
+ * @returns {object} Spawn instructions or error
29
+ */
30
+ function run(args = [], opts = {}) {
31
+ const brainDir = opts.brainDir || path.join(process.cwd(), '.brain');
32
+ const state = readState(brainDir);
33
+
34
+ if (!state) {
35
+ return { error: 'no-state' };
36
+ }
37
+
38
+ const mapper = getAgent('mapper');
39
+ const codebaseRoot = process.cwd();
40
+ const outputDir = path.join(codebaseRoot, '.brain', 'codebase');
41
+
42
+ // Parse --focus flag
43
+ let focusAreas = mapper.focus;
44
+ const focusIdx = args.indexOf('--focus');
45
+ if (focusIdx !== -1 && args[focusIdx + 1]) {
46
+ const requested = args[focusIdx + 1];
47
+ if (!FOCUS_OUTPUT_MAP[requested]) {
48
+ return { error: 'invalid-focus', message: `Unknown focus: ${requested}. Valid: ${Object.keys(FOCUS_OUTPUT_MAP).join(', ')}` };
49
+ }
50
+ focusAreas = [requested];
51
+ }
52
+
53
+ // Load mapper template
54
+ let template;
55
+ try {
56
+ template = loadTemplate(mapper.template);
57
+ } catch {
58
+ return { error: 'template-missing', message: `Could not load mapper template` };
59
+ }
60
+
61
+ // Build spawn instructions for each focus area
62
+ const focusAgents = focusAreas.map(focus => {
63
+ const prompt = interpolate(template, {
64
+ focus,
65
+ codebase_root: codebaseRoot,
66
+ output_dir: outputDir
67
+ });
68
+
69
+ const model = resolveModel('mapper', state);
70
+
71
+ return {
72
+ focus,
73
+ prompt,
74
+ model,
75
+ output_files: FOCUS_OUTPUT_MAP[focus]
76
+ };
77
+ });
78
+
79
+ // Read parallelization setting
80
+ const parallel = state.workflow?.mapper_parallelization ?? true;
81
+
82
+ // Run security scan on output directory
83
+ let scan_results = null;
84
+ try {
85
+ if (fs.existsSync(outputDir)) {
86
+ scan_results = scanFiles(outputDir);
87
+ }
88
+ } catch {
89
+ // Scan failure is non-fatal; leave scan_results as null
90
+ }
91
+
92
+ return {
93
+ action: 'spawn-mapper',
94
+ focus_agents: focusAgents,
95
+ parallel,
96
+ scan_results,
97
+ security_note: 'Security scan runs on .brain/codebase/ output directory. On first run scan_results will be null since mapper agents have not produced output yet.'
98
+ };
99
+ }
100
+
101
+ module.exports = { run };