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,306 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { output, error, success } = require('../core.cjs');
6
+ const {
7
+ CATEGORIES,
8
+ SCHEMA,
9
+ getConfig,
10
+ setConfig,
11
+ listConfig,
12
+ resetConfig,
13
+ exportConfig,
14
+ importConfig,
15
+ getSchema,
16
+ validateConfig,
17
+ suggestKey
18
+ } = require('../config.cjs');
19
+
20
+ /**
21
+ * Parse flags from args array (--global, --category).
22
+ * @param {string[]} args
23
+ * @returns {{ global: boolean, category: string|null, cleanArgs: string[] }}
24
+ */
25
+ function parseFlags(args) {
26
+ let global = false;
27
+ let category = null;
28
+ const cleanArgs = [];
29
+
30
+ for (let i = 0; i < args.length; i++) {
31
+ if (args[i] === '--global') {
32
+ global = true;
33
+ } else if (args[i] === '--category' && i + 1 < args.length) {
34
+ category = args[i + 1];
35
+ i++;
36
+ } else {
37
+ cleanArgs.push(args[i]);
38
+ }
39
+ }
40
+
41
+ return { global, category, cleanArgs };
42
+ }
43
+
44
+ /**
45
+ * Show usage help for all subcommands.
46
+ */
47
+ function showUsage() {
48
+ const lines = [
49
+ 'Usage: brain-dev config <subcommand> [args] [--global]',
50
+ '',
51
+ 'Subcommands:',
52
+ ' set <key> <value> Set a configuration value',
53
+ ' get <key> Get a configuration value',
54
+ ' list [--category X] List all settings grouped by category',
55
+ ' reset [key|category] Reset to defaults',
56
+ ' docs Show full schema reference',
57
+ ' export Export config as portable JSON',
58
+ ' import <file.json> Import config from file',
59
+ '',
60
+ 'Flags:',
61
+ ' --global Target ~/.brain/defaults.json',
62
+ ' --category <name> Filter by category'
63
+ ];
64
+ output(null, lines.join('\n'));
65
+ }
66
+
67
+ /**
68
+ * Run the config command.
69
+ * @param {string[]} args - CLI arguments after 'config'
70
+ * @param {string} brainDir - Path to .brain/ directory
71
+ * @returns {object} Structured result
72
+ */
73
+ async function run(args = [], brainDir) {
74
+ if (!brainDir) {
75
+ brainDir = path.join(process.cwd(), '.brain');
76
+ }
77
+
78
+ const { global: isGlobal, category, cleanArgs } = parseFlags(args);
79
+ const subcommand = cleanArgs[0];
80
+
81
+ switch (subcommand) {
82
+ case undefined: {
83
+ // Interactive category menu
84
+ const categorySettings = {};
85
+ for (const cat of CATEGORIES) {
86
+ const keys = Object.entries(SCHEMA).filter(([, e]) => e.category === cat);
87
+ categorySettings[cat] = keys.length;
88
+ }
89
+
90
+ const lines = ['Configuration Categories:', ''];
91
+ let idx = 1;
92
+ for (const [cat, count] of Object.entries(categorySettings)) {
93
+ lines.push(` ${idx}. ${cat} (${count} settings)`);
94
+ idx++;
95
+ }
96
+ lines.push('');
97
+ lines.push('Which category would you like to explore?');
98
+
99
+ output(null, lines.join('\n'));
100
+
101
+ return {
102
+ action: 'interactive',
103
+ categories: categorySettings
104
+ };
105
+ }
106
+
107
+ case 'set': {
108
+ const key = cleanArgs[1];
109
+ const value = cleanArgs[2];
110
+
111
+ if (!key || value === undefined) {
112
+ error('Usage: config set <key> <value>');
113
+ return { action: 'error', error: 'missing-args' };
114
+ }
115
+
116
+ const oldValue = getConfig(brainDir, key, { global: isGlobal });
117
+
118
+ try {
119
+ setConfig(brainDir, key, value, { global: isGlobal });
120
+ } catch (err) {
121
+ error(err.message);
122
+ return { action: 'error', error: err.message };
123
+ }
124
+
125
+ const newValue = getConfig(brainDir, key, { global: isGlobal });
126
+ success(`${key}: ${JSON.stringify(oldValue)} -> ${JSON.stringify(newValue)}`);
127
+
128
+ return {
129
+ action: 'set',
130
+ key,
131
+ oldValue,
132
+ newValue,
133
+ global: isGlobal || undefined
134
+ };
135
+ }
136
+
137
+ case 'get': {
138
+ const key = cleanArgs[1];
139
+
140
+ if (!key) {
141
+ error('Usage: config get <key>');
142
+ return { action: 'error', error: 'missing-key' };
143
+ }
144
+
145
+ const value = getConfig(brainDir, key, { global: isGlobal });
146
+
147
+ if (value === undefined) {
148
+ const suggestion = suggestKey(key, Object.keys(SCHEMA));
149
+ let msg = `Key "${key}" not found.`;
150
+ if (suggestion) msg += ` Did you mean: "${suggestion}"?`;
151
+ error(msg);
152
+ return { action: 'error', error: 'not-found', suggestion };
153
+ }
154
+
155
+ output(null, `${key} = ${JSON.stringify(value)}`);
156
+
157
+ return {
158
+ action: 'get',
159
+ key,
160
+ value,
161
+ global: isGlobal || undefined
162
+ };
163
+ }
164
+
165
+ case 'list': {
166
+ const opts = {};
167
+ if (category) opts.category = category;
168
+
169
+ const categories = listConfig(brainDir, opts);
170
+
171
+ const lines = [];
172
+ for (const [cat, entries] of Object.entries(categories)) {
173
+ lines.push(`[${cat}]`);
174
+ for (const entry of entries) {
175
+ const defaultMark = JSON.stringify(entry.value) === JSON.stringify(entry.default) ? '' : ` (default: ${JSON.stringify(entry.default)})`;
176
+ lines.push(` ${entry.key} = ${JSON.stringify(entry.value)}${defaultMark}`);
177
+ }
178
+ lines.push('');
179
+ }
180
+
181
+ output(null, lines.join('\n'));
182
+
183
+ return {
184
+ action: 'list',
185
+ categories
186
+ };
187
+ }
188
+
189
+ case 'reset': {
190
+ const target = cleanArgs[1];
191
+
192
+ try {
193
+ if (!target) {
194
+ resetConfig(brainDir);
195
+ success('All configuration reset to defaults.');
196
+ } else if (target.includes('.')) {
197
+ // Key reset
198
+ resetConfig(brainDir, target);
199
+ success(`Reset "${target}" to default.`);
200
+ } else if (CATEGORIES.includes(target)) {
201
+ // Category reset
202
+ resetConfig(brainDir, { category: target });
203
+ success(`Reset all "${target}" settings to defaults.`);
204
+ } else {
205
+ // Try as key first, then category
206
+ const validation = validateConfig(target, '');
207
+ if (validation.valid || SCHEMA[target]) {
208
+ resetConfig(brainDir, target);
209
+ success(`Reset "${target}" to default.`);
210
+ } else {
211
+ error(`Unknown key or category: "${target}"`);
212
+ return { action: 'error', error: 'unknown-target' };
213
+ }
214
+ }
215
+ } catch (err) {
216
+ error(err.message);
217
+ return { action: 'error', error: err.message };
218
+ }
219
+
220
+ return { action: 'reset', target: target || 'all' };
221
+ }
222
+
223
+ case 'docs': {
224
+ const schema = getSchema();
225
+ const byCat = {};
226
+ for (const [key, entry] of Object.entries(schema)) {
227
+ if (!byCat[entry.category]) byCat[entry.category] = [];
228
+ byCat[entry.category].push({ key, ...entry });
229
+ }
230
+
231
+ const lines = ['# Configuration Reference', ''];
232
+ for (const [cat, entries] of Object.entries(byCat)) {
233
+ lines.push(`## ${cat}`, '');
234
+ lines.push('| Key | Type | Default | Description |');
235
+ lines.push('|-----|------|---------|-------------|');
236
+ for (const entry of entries) {
237
+ const defStr = JSON.stringify(entry.default);
238
+ const typeStr = entry.type === 'enum' ? `enum(${entry.values.join('/')})` : entry.type;
239
+ lines.push(`| ${entry.key} | ${typeStr} | ${defStr} | ${entry.description} |`);
240
+ }
241
+ lines.push('');
242
+ }
243
+
244
+ output(null, lines.join('\n'));
245
+
246
+ return { action: 'docs', schema };
247
+ }
248
+
249
+ case 'export': {
250
+ try {
251
+ const json = exportConfig(brainDir);
252
+ // Raw JSON output for piping
253
+ console.log(json);
254
+ return { action: 'export', json };
255
+ } catch (err) {
256
+ error(err.message);
257
+ return { action: 'error', error: err.message };
258
+ }
259
+ }
260
+
261
+ case 'import': {
262
+ const filePath = cleanArgs[1];
263
+
264
+ if (!filePath) {
265
+ error('Usage: config import <file.json>');
266
+ return { action: 'error', error: 'missing-file' };
267
+ }
268
+
269
+ const resolved = path.resolve(filePath);
270
+ if (!fs.existsSync(resolved)) {
271
+ error(`File not found: ${resolved}`);
272
+ return { action: 'error', error: 'file-not-found' };
273
+ }
274
+
275
+ try {
276
+ const jsonString = fs.readFileSync(resolved, 'utf8');
277
+ const result = importConfig(brainDir, jsonString);
278
+
279
+ const lines = [`Imported: ${result.imported} settings`];
280
+ if (result.skipped.length > 0) {
281
+ lines.push(`Skipped: ${result.skipped.length}`);
282
+ for (const s of result.skipped) {
283
+ lines.push(` - ${s.key}: ${s.reason}`);
284
+ }
285
+ }
286
+ success(lines.join('\n'));
287
+
288
+ return {
289
+ action: 'import',
290
+ imported: result.imported,
291
+ skipped: result.skipped
292
+ };
293
+ } catch (err) {
294
+ error(err.message);
295
+ return { action: 'error', error: err.message };
296
+ }
297
+ }
298
+
299
+ default: {
300
+ showUsage();
301
+ return { action: 'usage' };
302
+ }
303
+ }
304
+ }
305
+
306
+ module.exports = { run };
@@ -0,0 +1,237 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { readState, writeState, atomicWriteSync } = require('../state.cjs');
6
+ const { parseRoadmap } = require('../roadmap.cjs');
7
+ const { loadTemplate, interpolate } = require('../templates.cjs');
8
+ const { output, error, success } = require('../core.cjs');
9
+
10
+ /**
11
+ * Find the phase directory under .brain/phases/ matching a phase number.
12
+ * Looks for directories starting with "NN-" (zero-padded).
13
+ * Creates the directory if it doesn't exist.
14
+ * @param {string} brainDir - Path to .brain/
15
+ * @param {number} phaseNumber
16
+ * @param {string} phaseName
17
+ * @returns {string} Path to phase directory
18
+ */
19
+ function ensurePhaseDir(brainDir, phaseNumber, phaseName) {
20
+ const phasesDir = path.join(brainDir, 'phases');
21
+ if (!fs.existsSync(phasesDir)) {
22
+ fs.mkdirSync(phasesDir, { recursive: true });
23
+ }
24
+
25
+ const padded = String(phaseNumber).padStart(2, '0');
26
+
27
+ // Look for existing directory with this number prefix
28
+ const existing = fs.readdirSync(phasesDir).find(d => d.startsWith(`${padded}-`));
29
+ if (existing) {
30
+ return path.join(phasesDir, existing);
31
+ }
32
+
33
+ // Create new directory
34
+ const dirName = `${padded}-${phaseName}`;
35
+ const dirPath = path.join(phasesDir, dirName);
36
+ fs.mkdirSync(dirPath, { recursive: true });
37
+ return dirPath;
38
+ }
39
+
40
+ /**
41
+ * Try to find and read a research SUMMARY.md for a phase.
42
+ * @param {string} brainDir
43
+ * @param {number} phaseNumber
44
+ * @returns {string|null}
45
+ */
46
+ function findResearchSummary(brainDir, phaseNumber) {
47
+ const phasesDir = path.join(brainDir, 'phases');
48
+ if (!fs.existsSync(phasesDir)) return null;
49
+
50
+ const padded = String(phaseNumber).padStart(2, '0');
51
+ const dirs = fs.readdirSync(phasesDir).filter(d => d.startsWith(`${padded}-`));
52
+
53
+ for (const dir of dirs) {
54
+ const summaryPath = path.join(phasesDir, dir, 'SUMMARY.md');
55
+ if (fs.existsSync(summaryPath)) {
56
+ return fs.readFileSync(summaryPath, 'utf8');
57
+ }
58
+ }
59
+ return null;
60
+ }
61
+
62
+ /**
63
+ * Run the discuss command.
64
+ *
65
+ * Analyze mode (default): Output gray-area analysis instructions for Claude.
66
+ * Save mode (--save --decisions <json>): Persist decisions to CONTEXT.md.
67
+ *
68
+ * @param {string[]} args - CLI arguments
69
+ * @param {object} [opts] - Options (brainDir for testing)
70
+ * @returns {object} Structured result
71
+ */
72
+ async function run(args = [], opts = {}) {
73
+ const brainDir = opts.brainDir || path.join(process.cwd(), '.brain');
74
+ const state = readState(brainDir);
75
+
76
+ if (!state) {
77
+ error("No brain state found. Run 'brain-dev init' first.");
78
+ return { error: 'no-state' };
79
+ }
80
+
81
+ const isSave = args.includes('--save');
82
+
83
+ if (isSave) {
84
+ return handleSave(args, brainDir, state);
85
+ }
86
+
87
+ return handleAnalyze(args, brainDir, state);
88
+ }
89
+
90
+ /**
91
+ * Handle analyze mode: output gray-area analysis instructions.
92
+ */
93
+ function handleAnalyze(args, brainDir, state) {
94
+ // Parse phase number: supports both `discuss 1` and `discuss --phase 1`
95
+ const phaseIdx = args.indexOf('--phase');
96
+ let phaseNumber;
97
+ if (phaseIdx >= 0) {
98
+ phaseNumber = parseInt(args[phaseIdx + 1], 10);
99
+ } else {
100
+ // Check for positional argument (first non-flag arg)
101
+ const positional = args.find(a => !a.startsWith('--') && !isNaN(parseInt(a, 10)));
102
+ phaseNumber = positional ? parseInt(positional, 10) : state.phase.current;
103
+ }
104
+
105
+ // Read roadmap for phase details
106
+ const roadmap = parseRoadmap(brainDir);
107
+ const phase = roadmap.phases.find(p => p.number === phaseNumber);
108
+
109
+ if (!phase) {
110
+ const available = roadmap.phases.map(p => p.number).join(', ') || 'none';
111
+ error(`Phase ${phaseNumber} not found in roadmap. Available phases: ${available}. Run /brain:progress to see current state.`);
112
+ return { error: 'phase-not-found' };
113
+ }
114
+
115
+ // Check for research summary
116
+ const researchSummary = findResearchSummary(brainDir, phaseNumber);
117
+
118
+ // Load and interpolate template
119
+ const template = loadTemplate('discuss');
120
+ const researchSection = researchSummary
121
+ ? `**Research Summary:**\n${researchSummary}`
122
+ : '_No research summary available for this phase._';
123
+
124
+ const prompt = interpolate(template, {
125
+ phase_number: phaseNumber,
126
+ phase_name: phase.name,
127
+ phase_goal: phase.goal,
128
+ phase_requirements: phase.requirements.join(', ') || 'None specified',
129
+ research_section: researchSection
130
+ });
131
+
132
+ // Update status to discussing
133
+ state.phase.status = 'discussing';
134
+ writeState(brainDir, state);
135
+
136
+ const result = {
137
+ action: 'discuss-phase',
138
+ phase: phaseNumber,
139
+ goal: phase.goal,
140
+ requirements: phase.requirements,
141
+ research_summary: researchSummary || null,
142
+ prompt,
143
+ nextAction: '/brain:plan'
144
+ };
145
+
146
+ // Human-readable output
147
+ const humanLines = [
148
+ `[brain] Discussion analysis for Phase ${phaseNumber}: ${phase.name}`,
149
+ '',
150
+ prompt
151
+ ];
152
+ output(result, humanLines.join('\n'));
153
+
154
+ return result;
155
+ }
156
+
157
+ /**
158
+ * Handle save mode: create CONTEXT.md with decisions.
159
+ */
160
+ function handleSave(args, brainDir, state) {
161
+ const decisionsIdx = args.indexOf('--decisions');
162
+ if (decisionsIdx < 0 || !args[decisionsIdx + 1]) {
163
+ error('--save requires --decisions <json>');
164
+ return { error: 'missing-decisions' };
165
+ }
166
+
167
+ let decisionsData;
168
+ try {
169
+ decisionsData = JSON.parse(args[decisionsIdx + 1]);
170
+ } catch {
171
+ error('Invalid JSON for --decisions');
172
+ return { error: 'invalid-json' };
173
+ }
174
+
175
+ const phaseNumber = state.phase.current;
176
+ const roadmap = parseRoadmap(brainDir);
177
+ const phase = roadmap.phases.find(p => p.number === phaseNumber);
178
+ const phaseName = phase ? phase.name : `Phase-${phaseNumber}`;
179
+
180
+ // Ensure phase directory exists
181
+ const phaseDir = ensurePhaseDir(brainDir, phaseNumber, phaseName);
182
+
183
+ // Build CONTEXT.md content
184
+ const decisions = decisionsData.decisions || [];
185
+ const specifics = decisionsData.specifics || [];
186
+ const deferred = decisionsData.deferred || [];
187
+
188
+ const lines = [
189
+ `# Phase ${phaseNumber}: ${phaseName} -- Context`,
190
+ '',
191
+ '<decisions>',
192
+ '',
193
+ '## Locked Decisions',
194
+ '',
195
+ ...decisions.map(d => `- ${d}`),
196
+ '',
197
+ '</decisions>',
198
+ '',
199
+ '<specifics>',
200
+ '',
201
+ '## Specific Approaches',
202
+ '',
203
+ ...specifics.map(s => `- ${s}`),
204
+ '',
205
+ '</specifics>',
206
+ '',
207
+ '<deferred>',
208
+ '',
209
+ '## Deferred Ideas',
210
+ '',
211
+ ...deferred.map(d => `- ${d}`),
212
+ '',
213
+ '</deferred>',
214
+ ''
215
+ ];
216
+
217
+ const contextPath = path.join(phaseDir, 'CONTEXT.md');
218
+ atomicWriteSync(contextPath, lines.join('\n'));
219
+
220
+ // Update state: set phase status to "discussed"
221
+ state.phase.status = 'discussed';
222
+ writeState(brainDir, state);
223
+
224
+ const result = {
225
+ action: 'context-saved',
226
+ phase: phaseNumber,
227
+ path: contextPath,
228
+ nextAction: '/brain:plan'
229
+ };
230
+
231
+ success("Context captured. Run /brain:plan to create execution plans.");
232
+ output(result, '');
233
+
234
+ return result;
235
+ }
236
+
237
+ module.exports = { run };