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,447 @@
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 { resolveModel } = require('../agents.cjs');
8
+ const { logEvent } = require('../logger.cjs');
9
+ const { output, error, success } = require('../core.cjs');
10
+
11
+ /**
12
+ * Generate a URL-safe slug from a description.
13
+ * @param {string} text
14
+ * @returns {string}
15
+ */
16
+ function generateSlug(text) {
17
+ return text
18
+ .toLowerCase()
19
+ .replace(/[^a-z0-9]+/g, '-')
20
+ .replace(/^-+|-+$/g, '')
21
+ .substring(0, 40);
22
+ }
23
+
24
+ /**
25
+ * Calculate the next quick task number by scanning .brain/quick/.
26
+ * @param {string} quickDir
27
+ * @returns {number}
28
+ */
29
+ function calculateNextNum(quickDir) {
30
+ try {
31
+ const existing = fs.readdirSync(quickDir)
32
+ .filter(f => /^\d+-/.test(f))
33
+ .map(f => parseInt(f.split('-')[0], 10))
34
+ .filter(n => !isNaN(n));
35
+ return existing.length > 0 ? Math.max(...existing) + 1 : 1;
36
+ } catch {
37
+ return 1;
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Find a quick task directory by number.
43
+ * @param {string} brainDir
44
+ * @param {number} taskNum
45
+ * @returns {string|null}
46
+ */
47
+ function findTaskDir(brainDir, taskNum) {
48
+ const quickDir = path.join(brainDir, 'quick');
49
+ if (!fs.existsSync(quickDir)) return null;
50
+ const match = fs.readdirSync(quickDir).find(d => d.startsWith(`${taskNum}-`));
51
+ return match ? path.join(quickDir, match) : null;
52
+ }
53
+
54
+ /**
55
+ * Parse --task N from args.
56
+ * @param {string[]} args
57
+ * @returns {number|null}
58
+ */
59
+ function getTaskNum(args) {
60
+ const idx = args.indexOf('--task');
61
+ if (idx >= 0 && args[idx + 1]) {
62
+ const num = parseInt(args[idx + 1], 10);
63
+ return isNaN(num) ? null : num;
64
+ }
65
+ return null;
66
+ }
67
+
68
+ /**
69
+ * Run the quick command.
70
+ *
71
+ * Steps (each triggered by flags):
72
+ * (default) → Init + planner instructions
73
+ * --execute --task N → Executor instructions
74
+ * --verify --task N → Verifier instructions (--full only)
75
+ * --complete --task N → STATE.md update + commit instructions
76
+ *
77
+ * @param {string[]} args
78
+ * @param {object} [opts]
79
+ */
80
+ async function run(args = [], opts = {}) {
81
+ const brainDir = opts.brainDir || path.join(process.cwd(), '.brain');
82
+ const state = readState(brainDir);
83
+
84
+ if (!state) {
85
+ error("No brain state found. Run 'brain-dev init' first.");
86
+ return { error: 'no-state' };
87
+ }
88
+
89
+ if (!state.project || !state.project.initialized) {
90
+ error("Project not initialized. Run '/brain:new-project' first.");
91
+ return { error: 'not-initialized' };
92
+ }
93
+
94
+ // Route to step handlers
95
+ if (args.includes('--complete')) {
96
+ return handleComplete(args, brainDir, state);
97
+ }
98
+ if (args.includes('--verify')) {
99
+ return handleVerify(args, brainDir, state);
100
+ }
101
+ if (args.includes('--execute')) {
102
+ return handleExecute(args, brainDir, state);
103
+ }
104
+
105
+ // Default: init + planner
106
+ return handleInit(args, brainDir, state);
107
+ }
108
+
109
+ /**
110
+ * Step 1-2: Parse args, create task dir, generate planner instructions.
111
+ */
112
+ function handleInit(args, brainDir, state) {
113
+ const isFull = args.includes('--full');
114
+
115
+ // Extract description (everything except flags)
116
+ const desc = args.filter(a => !a.startsWith('--')).join(' ').trim();
117
+
118
+ if (!desc) {
119
+ const humanText = [
120
+ '[brain] Quick Task',
121
+ '',
122
+ 'IMPORTANT: Use AskUserQuestion to ask what the user wants to do.',
123
+ 'Then run: npx brain-dev quick "description here"'
124
+ ].join('\n');
125
+
126
+ const result = {
127
+ action: 'ask-description',
128
+ tool: 'AskUserQuestion',
129
+ question: { question: 'What do you want to do?', header: 'Quick Task', options: [
130
+ { label: 'Fix a bug', description: 'Fix an existing issue or error' },
131
+ { label: 'Add a feature', description: 'Add new functionality' },
132
+ { label: 'Refactor', description: 'Improve code quality without changing behavior' },
133
+ { label: 'Update docs', description: 'Update documentation or README' }
134
+ ], multiSelect: false },
135
+ instruction: 'After getting description, run: npx brain-dev quick "user description here"'
136
+ };
137
+
138
+ output(result, humanText);
139
+ return result;
140
+ }
141
+
142
+ // Create quick task directory
143
+ const quickDir = path.join(brainDir, 'quick');
144
+ fs.mkdirSync(quickDir, { recursive: true });
145
+ const nextNum = calculateNextNum(quickDir);
146
+ const slug = generateSlug(desc);
147
+ const taskDir = path.join(quickDir, `${nextNum}-${slug}`);
148
+ fs.mkdirSync(taskDir, { recursive: true });
149
+
150
+ // Save task metadata
151
+ const taskMeta = { num: nextNum, slug, description: desc, full: isFull, created: new Date().toISOString() };
152
+ fs.writeFileSync(path.join(taskDir, 'task.json'), JSON.stringify(taskMeta, null, 2));
153
+
154
+ // Generate planner prompt using existing template + quick constraints
155
+ const template = loadTemplate('planner');
156
+ const prompt = interpolate(template, {
157
+ phase_number: nextNum,
158
+ phase_name: desc,
159
+ phase_goal: desc,
160
+ phase_requirements: 'Quick task — single focused deliverable',
161
+ phase_depends_on: 'None',
162
+ context_decisions: '_Quick mode: no CONTEXT.md required._',
163
+ research_summary: '_Quick mode: no research phase._',
164
+ output_dir: taskDir,
165
+ phase_number_padded: String(nextNum).padStart(3, '0'),
166
+ phase_slug: slug
167
+ });
168
+
169
+ const quickConstraints = [
170
+ '',
171
+ '## Quick Mode Constraints',
172
+ '',
173
+ '- Create a SINGLE plan file: PLAN-1.md',
174
+ '- Maximum 1-2 focused tasks',
175
+ '- Target ~30% context budget (keep it small and focused)',
176
+ '- No research or context gathering needed',
177
+ '- Task should be atomic and self-contained',
178
+ isFull ? '- MUST include must_haves in frontmatter (truths, artifacts, key_links)' : '',
179
+ isFull ? '- Each task MUST have files, action, verify, done fields' : '',
180
+ '',
181
+ `Write plan to: ${path.join(taskDir, 'PLAN-1.md')}`,
182
+ ''
183
+ ].filter(Boolean).join('\n');
184
+
185
+ const fullPrompt = prompt + quickConstraints;
186
+ const model = resolveModel('planner', state);
187
+
188
+ logEvent(brainDir, 0, { type: 'quick-init', task: nextNum, description: desc, full: isFull });
189
+
190
+ const humanText = [
191
+ `[brain] Quick Task ${nextNum}: ${desc}`,
192
+ isFull ? '[brain] Full mode: plan checking + verification enabled' : '',
193
+ `[brain] Directory: ${taskDir}`,
194
+ '',
195
+ 'IMPORTANT: Use the Agent tool (subagent_type: "brain-planner") to spawn the planner.',
196
+ 'Do NOT plan the task yourself.',
197
+ '',
198
+ `After planner completes, run: npx brain-dev quick --execute --task ${nextNum}`,
199
+ '',
200
+ fullPrompt
201
+ ].filter(Boolean).join('\n');
202
+
203
+ const result = {
204
+ action: 'spawn-quick-planner',
205
+ task: nextNum,
206
+ taskDir,
207
+ slug,
208
+ isFull,
209
+ prompt: fullPrompt,
210
+ model,
211
+ nextAction: `npx brain-dev quick --execute --task ${nextNum}`
212
+ };
213
+
214
+ output(result, humanText);
215
+ return result;
216
+ }
217
+
218
+ /**
219
+ * Step 5: Generate executor instructions for a quick task.
220
+ */
221
+ function handleExecute(args, brainDir, state) {
222
+ const taskNum = getTaskNum(args);
223
+ if (!taskNum) {
224
+ error('--execute requires --task <number>');
225
+ return { error: 'missing-task-number' };
226
+ }
227
+
228
+ const taskDir = findTaskDir(brainDir, taskNum);
229
+ if (!taskDir) {
230
+ error(`Quick task ${taskNum} not found.`);
231
+ return { error: 'task-not-found' };
232
+ }
233
+
234
+ // Read plan
235
+ const planPath = path.join(taskDir, 'PLAN-1.md');
236
+ if (!fs.existsSync(planPath)) {
237
+ error(`Plan not found for task ${taskNum}. Run quick task init first.`);
238
+ return { error: 'no-plan' };
239
+ }
240
+
241
+ const planContent = fs.readFileSync(planPath, 'utf8');
242
+ const summaryPath = path.join(taskDir, 'SUMMARY-1.md');
243
+
244
+ // Load executor template
245
+ const template = loadTemplate('executor');
246
+ const prompt = interpolate(template, {
247
+ plan_path: planPath,
248
+ summary_path: summaryPath,
249
+ plan_content: planContent,
250
+ phase: `Q${taskNum}`,
251
+ plan_number: '1',
252
+ subsystem: `quick-${taskNum}`
253
+ });
254
+
255
+ const model = resolveModel('executor', state);
256
+
257
+ // Read task metadata for --full check
258
+ let isFull = false;
259
+ try {
260
+ const meta = JSON.parse(fs.readFileSync(path.join(taskDir, 'task.json'), 'utf8'));
261
+ isFull = meta.full === true;
262
+ } catch { /* ignore */ }
263
+
264
+ const nextCmd = isFull
265
+ ? `npx brain-dev quick --verify --task ${taskNum}`
266
+ : `npx brain-dev quick --complete --task ${taskNum}`;
267
+
268
+ logEvent(brainDir, 0, { type: 'quick-execute', task: taskNum });
269
+
270
+ const humanText = [
271
+ `[brain] Executing Quick Task ${taskNum}`,
272
+ `[brain] Plan: ${planPath}`,
273
+ `[brain] Summary: ${summaryPath}`,
274
+ '',
275
+ 'IMPORTANT: Use the Agent tool (subagent_type: "brain-executor") to spawn the executor.',
276
+ 'Do NOT execute the tasks yourself.',
277
+ '',
278
+ `After executor completes, run: ${nextCmd}`,
279
+ '',
280
+ prompt
281
+ ].join('\n');
282
+
283
+ const result = {
284
+ action: 'spawn-quick-executor',
285
+ task: taskNum,
286
+ planPath,
287
+ summaryPath,
288
+ prompt,
289
+ model,
290
+ nextAction: nextCmd
291
+ };
292
+
293
+ output(result, humanText);
294
+ return result;
295
+ }
296
+
297
+ /**
298
+ * Step 6: Generate verifier instructions (--full mode only).
299
+ */
300
+ function handleVerify(args, brainDir, state) {
301
+ const taskNum = getTaskNum(args);
302
+ if (!taskNum) {
303
+ error('--verify requires --task <number>');
304
+ return { error: 'missing-task-number' };
305
+ }
306
+
307
+ const taskDir = findTaskDir(brainDir, taskNum);
308
+ if (!taskDir) {
309
+ error(`Quick task ${taskNum} not found.`);
310
+ return { error: 'task-not-found' };
311
+ }
312
+
313
+ const planPath = path.join(taskDir, 'PLAN-1.md');
314
+ const verificationPath = path.join(taskDir, 'VERIFICATION-1.md');
315
+
316
+ // Load verifier template
317
+ const template = loadTemplate('verifier');
318
+
319
+ // Extract must_haves from plan
320
+ let mustHaves = 'No must_haves found in plan.';
321
+ try {
322
+ const planContent = fs.readFileSync(planPath, 'utf8');
323
+ const truthsMatch = planContent.match(/truths:\s*\n((?:\s+-\s+"[^"]*"\n?)*)/);
324
+ if (truthsMatch) {
325
+ mustHaves = truthsMatch[0];
326
+ }
327
+ } catch { /* ignore */ }
328
+
329
+ const prompt = interpolate(template, {
330
+ must_haves: mustHaves,
331
+ output_path: verificationPath,
332
+ anti_pattern_results: 'No anti-pattern scan for quick tasks.',
333
+ nyquist_section: 'Nyquist validation not applicable for quick tasks.'
334
+ });
335
+
336
+ const model = resolveModel('verifier', state);
337
+
338
+ logEvent(brainDir, 0, { type: 'quick-verify', task: taskNum });
339
+
340
+ const humanText = [
341
+ `[brain] Verifying Quick Task ${taskNum}`,
342
+ '',
343
+ 'IMPORTANT: Use the Agent tool (subagent_type: "brain-verifier") to spawn the verifier.',
344
+ '',
345
+ `After verification, run: npx brain-dev quick --complete --task ${taskNum}`,
346
+ '',
347
+ prompt
348
+ ].join('\n');
349
+
350
+ const result = {
351
+ action: 'spawn-quick-verifier',
352
+ task: taskNum,
353
+ prompt,
354
+ model,
355
+ verificationPath,
356
+ nextAction: `npx brain-dev quick --complete --task ${taskNum}`
357
+ };
358
+
359
+ output(result, humanText);
360
+ return result;
361
+ }
362
+
363
+ /**
364
+ * Step 7-8: Update STATE.md and output commit instructions.
365
+ */
366
+ function handleComplete(args, brainDir, state) {
367
+ const taskNum = getTaskNum(args);
368
+ if (!taskNum) {
369
+ error('--complete requires --task <number>');
370
+ return { error: 'missing-task-number' };
371
+ }
372
+
373
+ const taskDir = findTaskDir(brainDir, taskNum);
374
+ if (!taskDir) {
375
+ error(`Quick task ${taskNum} not found.`);
376
+ return { error: 'task-not-found' };
377
+ }
378
+
379
+ // Read task metadata
380
+ let meta = { description: 'Unknown task', full: false };
381
+ try {
382
+ meta = JSON.parse(fs.readFileSync(path.join(taskDir, 'task.json'), 'utf8'));
383
+ } catch { /* ignore */ }
384
+
385
+ const date = new Date().toISOString().split('T')[0];
386
+ const slug = path.basename(taskDir).replace(/^\d+-/, '');
387
+
388
+ // Determine status
389
+ let status = 'Done';
390
+ if (meta.full) {
391
+ const verPath = path.join(taskDir, 'VERIFICATION-1.md');
392
+ if (fs.existsSync(verPath)) {
393
+ const content = fs.readFileSync(verPath, 'utf8');
394
+ if (content.includes('status: passed')) status = 'Verified';
395
+ else if (content.includes('status: human_needed')) status = 'Needs Review';
396
+ else if (content.includes('status: gaps_found')) status = 'Gaps';
397
+ }
398
+ }
399
+
400
+ // Update quick task count in state
401
+ if (!state.quick) state.quick = { count: 0 };
402
+ state.quick.count = taskNum;
403
+ writeState(brainDir, state);
404
+
405
+ logEvent(brainDir, 0, { type: 'quick-complete', task: taskNum, status });
406
+
407
+ // Build commit file list
408
+ const filesToCommit = [];
409
+ for (const f of ['PLAN-1.md', 'SUMMARY-1.md', 'VERIFICATION-1.md', 'task.json']) {
410
+ if (fs.existsSync(path.join(taskDir, f))) {
411
+ filesToCommit.push(path.relative(path.dirname(brainDir), path.join(taskDir, f)));
412
+ }
413
+ }
414
+ filesToCommit.push('.brain/brain.json');
415
+ filesToCommit.push('.brain/STATE.md');
416
+
417
+ const humanText = [
418
+ `[brain] Quick Task ${taskNum} Complete!`,
419
+ '',
420
+ `Task: ${meta.description}`,
421
+ `Status: ${status}`,
422
+ `Directory: ${taskDir}`,
423
+ '',
424
+ 'Update STATE.md with a "Quick Tasks" section if not present, add this row:',
425
+ `| ${taskNum} | ${meta.description} | ${date} | ${status} | [${taskNum}-${slug}](./quick/${taskNum}-${slug}/) |`,
426
+ '',
427
+ `Then commit: git add ${filesToCommit.join(' ')} && git commit -m "quick(${taskNum}): ${meta.description}"`,
428
+ '',
429
+ 'Ready for next: /brain:quick or /brain:progress'
430
+ ].join('\n');
431
+
432
+ const result = {
433
+ action: 'quick-complete',
434
+ task: taskNum,
435
+ description: meta.description,
436
+ status,
437
+ date,
438
+ filesToCommit,
439
+ commitMessage: `quick(${taskNum}): ${meta.description}`,
440
+ nextAction: '/brain:quick'
441
+ };
442
+
443
+ output(result, humanText);
444
+ return result;
445
+ }
446
+
447
+ module.exports = { run, generateSlug, calculateNextNum, findTaskDir };
@@ -0,0 +1,196 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { readState } = require('../state.cjs');
6
+ const { output, prefix, error } = require('../core.cjs');
7
+
8
+ /**
9
+ * Parse simple YAML frontmatter from markdown content.
10
+ * @param {string} content - Markdown with YAML frontmatter
11
+ * @returns {{ frontmatter: object, body: string }}
12
+ */
13
+ function parseFrontmatter(content) {
14
+ const fm = { frontmatter: {}, body: content };
15
+
16
+ if (!content.startsWith('---')) return fm;
17
+
18
+ const endIdx = content.indexOf('---', 3);
19
+ if (endIdx === -1) return fm;
20
+
21
+ const yamlBlock = content.slice(3, endIdx).trim();
22
+ const body = content.slice(endIdx + 3).trim();
23
+
24
+ const frontmatter = {};
25
+ for (const line of yamlBlock.split('\n')) {
26
+ const colonIdx = line.indexOf(':');
27
+ if (colonIdx === -1) continue;
28
+ const key = line.slice(0, colonIdx).trim();
29
+ let value = line.slice(colonIdx + 1).trim();
30
+ // Try to parse numbers
31
+ if (/^\d+$/.test(value)) value = parseInt(value, 10);
32
+ frontmatter[key] = value;
33
+ }
34
+
35
+ fm.frontmatter = frontmatter;
36
+ fm.body = body;
37
+ return fm;
38
+ }
39
+
40
+ /**
41
+ * Extract named sections from markdown body.
42
+ * Sections start with ## heading.
43
+ * @param {string} body - Markdown body (after frontmatter)
44
+ * @returns {object} Map of camelCase section names to content
45
+ */
46
+ function parseSections(body) {
47
+ const sections = {};
48
+ const lines = body.split('\n');
49
+ let currentKey = null;
50
+ let currentLines = [];
51
+
52
+ for (const line of lines) {
53
+ if (line.startsWith('## ')) {
54
+ // Save previous section
55
+ if (currentKey) {
56
+ sections[currentKey] = currentLines.join('\n').trim();
57
+ }
58
+ // New section
59
+ const heading = line.slice(3).trim();
60
+ currentKey = heading
61
+ .replace(/[^a-zA-Z0-9\s]/g, '')
62
+ .split(/\s+/)
63
+ .map((w, i) => i === 0 ? w.toLowerCase() : w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
64
+ .join('');
65
+ currentLines = [];
66
+ } else if (currentKey) {
67
+ currentLines.push(line);
68
+ }
69
+ }
70
+
71
+ // Save last section
72
+ if (currentKey) {
73
+ sections[currentKey] = currentLines.join('\n').trim();
74
+ }
75
+
76
+ return sections;
77
+ }
78
+
79
+ /**
80
+ * Run the resume command.
81
+ * @param {string[]} args - CLI arguments
82
+ * @param {object} [opts] - Options (brainDir for testing)
83
+ * @returns {object} Result
84
+ */
85
+ async function run(args = [], opts = {}) {
86
+ const brainDir = opts.brainDir || path.join(process.cwd(), '.brain');
87
+ const state = readState(brainDir);
88
+
89
+ // Check --session flag
90
+ const sessionIdx = args.indexOf('--session');
91
+ const sessionId = sessionIdx !== -1 && sessionIdx + 1 < args.length ? args[sessionIdx + 1] : null;
92
+
93
+ let snapshotContent = null;
94
+ let snapshotSource = null;
95
+
96
+ if (sessionId) {
97
+ // Load specific session
98
+ const sessionPath = path.join(brainDir, 'sessions', `${sessionId}.md`);
99
+ if (fs.existsSync(sessionPath)) {
100
+ snapshotContent = fs.readFileSync(sessionPath, 'utf8');
101
+ snapshotSource = sessionPath;
102
+ } else {
103
+ error(`Session not found: ${sessionId}`);
104
+ return { error: `Session not found: ${sessionId}` };
105
+ }
106
+ } else {
107
+ // Check for continue-here.md
108
+ const defaultPath = path.join(brainDir, 'continue-here.md');
109
+ if (fs.existsSync(defaultPath) && fs.statSync(defaultPath).isFile()) {
110
+ snapshotContent = fs.readFileSync(defaultPath, 'utf8');
111
+ snapshotSource = defaultPath;
112
+ } else {
113
+ // Check if sessions directory has files
114
+ const sessionsDir = path.join(brainDir, 'sessions');
115
+ if (fs.existsSync(sessionsDir)) {
116
+ const sessionFiles = fs.readdirSync(sessionsDir)
117
+ .filter(f => f.endsWith('.md'))
118
+ .sort();
119
+
120
+ if (sessionFiles.length > 0) {
121
+ // List available sessions
122
+ const sessions = sessionFiles.map(f => {
123
+ const content = fs.readFileSync(path.join(sessionsDir, f), 'utf8');
124
+ const { body } = parseFrontmatter(content);
125
+ const sections = parseSections(body);
126
+ const firstLine = (sections.currentState || '').split('\n')[0] || '';
127
+ return {
128
+ id: f.replace('.md', ''),
129
+ file: f,
130
+ summary: firstLine
131
+ };
132
+ });
133
+
134
+ output(
135
+ { sessions },
136
+ [
137
+ prefix('Multiple sessions available. Use --session <id> to select:'),
138
+ ...sessions.map(s => ` ${s.id} ${s.summary}`)
139
+ ].join('\n')
140
+ );
141
+ return { sessions };
142
+ }
143
+ }
144
+
145
+ error('No paused session found. Use /brain:pause to save a session first.');
146
+ return { error: 'No paused session found' };
147
+ }
148
+ }
149
+
150
+ // Parse snapshot
151
+ const { frontmatter, body } = parseFrontmatter(snapshotContent);
152
+ const sections = parseSections(body);
153
+
154
+ // Build briefing
155
+ const briefingLines = [
156
+ prefix('=== Session Briefing ==='),
157
+ '',
158
+ prefix(`Phase: ${frontmatter.phase || '?'} (${frontmatter.status || '?'})`),
159
+ prefix(`Paused at: ${frontmatter.paused_at || 'unknown'}`),
160
+ ''
161
+ ];
162
+
163
+ if (sections.currentState) {
164
+ briefingLines.push(prefix('Current State:'));
165
+ briefingLines.push(` ${sections.currentState}`);
166
+ briefingLines.push('');
167
+ }
168
+ if (sections.completedWork) {
169
+ briefingLines.push(prefix('Completed:'));
170
+ for (const line of sections.completedWork.split('\n').filter(l => l.trim())) {
171
+ briefingLines.push(` ${line}`);
172
+ }
173
+ briefingLines.push('');
174
+ }
175
+ if (sections.remainingWork) {
176
+ briefingLines.push(prefix('Remaining:'));
177
+ for (const line of sections.remainingWork.split('\n').filter(l => l.trim())) {
178
+ briefingLines.push(` ${line}`);
179
+ }
180
+ briefingLines.push('');
181
+ }
182
+ if (sections.nextAction) {
183
+ briefingLines.push(prefix(`Next: ${sections.nextAction}`));
184
+ }
185
+
186
+ const result = {
187
+ briefing: true,
188
+ snapshot: { frontmatter, sections },
189
+ source: snapshotSource
190
+ };
191
+
192
+ output(result, briefingLines.join('\n'));
193
+ return result;
194
+ }
195
+
196
+ module.exports = { run };