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,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 };
|