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,451 @@
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 { parseRoadmap } = require('../roadmap.cjs');
7
+ const { loadTemplate, interpolate } = require('../templates.cjs');
8
+ const { getAgent, resolveModel } = require('../agents.cjs');
9
+ const { logEvent } = require('../logger.cjs');
10
+ const { estimateFromPlan, getDefaultBudget, checkBudget } = require('../complexity.cjs');
11
+ const { output, error, success } = require('../core.cjs');
12
+
13
+ /**
14
+ * Find a phase directory under .brain/phases/ matching a phase number.
15
+ * @param {string} brainDir
16
+ * @param {number} phaseNumber
17
+ * @returns {string|null} Path to phase directory, or null
18
+ */
19
+ function findPhaseDir(brainDir, phaseNumber) {
20
+ const phasesDir = path.join(brainDir, 'phases');
21
+ if (!fs.existsSync(phasesDir)) return null;
22
+
23
+ const padded = String(phaseNumber).padStart(2, '0');
24
+ const match = fs.readdirSync(phasesDir).find(d => d.startsWith(`${padded}-`));
25
+ return match ? path.join(phasesDir, match) : null;
26
+ }
27
+
28
+ /**
29
+ * Read CONTEXT.md content for a phase if it exists.
30
+ * @param {string} brainDir
31
+ * @param {number} phaseNumber
32
+ * @returns {string|null}
33
+ */
34
+ function readContext(brainDir, phaseNumber) {
35
+ const phaseDir = findPhaseDir(brainDir, phaseNumber);
36
+ if (!phaseDir) return null;
37
+
38
+ const contextPath = path.join(phaseDir, 'CONTEXT.md');
39
+ if (!fs.existsSync(contextPath)) return null;
40
+ return fs.readFileSync(contextPath, 'utf8');
41
+ }
42
+
43
+ /**
44
+ * Read research SUMMARY.md for a phase if it exists.
45
+ * @param {string} brainDir
46
+ * @param {number} phaseNumber
47
+ * @returns {string|null}
48
+ */
49
+ function readResearchSummary(brainDir, phaseNumber) {
50
+ const phaseDir = findPhaseDir(brainDir, phaseNumber);
51
+ if (!phaseDir) return null;
52
+
53
+ const summaryPath = path.join(phaseDir, 'SUMMARY.md');
54
+ if (!fs.existsSync(summaryPath)) return null;
55
+ return fs.readFileSync(summaryPath, 'utf8');
56
+ }
57
+
58
+ /**
59
+ * Generate the planner prompt for a single phase.
60
+ * @param {object} phase - Phase data from roadmap
61
+ * @param {string} brainDir
62
+ * @returns {{ prompt: string, output_dir: string }}
63
+ */
64
+ function generatePlannerPrompt(phase, brainDir) {
65
+ const template = loadTemplate('planner');
66
+
67
+ const contextContent = readContext(brainDir, phase.number);
68
+ const researchContent = readResearchSummary(brainDir, phase.number);
69
+
70
+ const contextSection = contextContent
71
+ ? `**Context Decisions:**\n${contextContent}`
72
+ : '_No CONTEXT.md found. Consider running /brain:discuss first._';
73
+
74
+ const researchSection = researchContent
75
+ ? `**Research Summary:**\n${researchContent}`
76
+ : '_No research summary available._';
77
+
78
+ const padded = String(phase.number).padStart(2, '0');
79
+ const slug = phase.name.toLowerCase().replace(/\s+/g, '-');
80
+ const phaseDir = findPhaseDir(brainDir, phase.number);
81
+ const outputDir = phaseDir || path.join(brainDir, 'phases', `${padded}-${slug}`);
82
+
83
+ const prompt = interpolate(template, {
84
+ phase_number: phase.number,
85
+ phase_name: phase.name,
86
+ phase_goal: phase.goal,
87
+ phase_requirements: phase.requirements.join(', ') || 'None specified',
88
+ phase_depends_on: phase.dependsOn.length > 0 ? phase.dependsOn.join(', ') : 'None',
89
+ context_decisions: contextSection,
90
+ research_summary: researchSection,
91
+ output_dir: outputDir,
92
+ phase_number_padded: padded,
93
+ phase_slug: slug
94
+ });
95
+
96
+ return { prompt, output_dir: outputDir };
97
+ }
98
+
99
+ /**
100
+ * Generate a plan-checker prompt for validating an existing plan.
101
+ *
102
+ * @param {string} planContent - Content of the PLAN.md file
103
+ * @param {string} phaseRequirements - Phase requirements string
104
+ * @param {string} contextDecisions - Context decisions content
105
+ * @param {string} phaseGoal - Phase goal description
106
+ * @returns {string} Interpolated checker prompt
107
+ */
108
+ function generateCheckerPrompt(planContent, phaseRequirements, contextDecisions, phaseGoal) {
109
+ const template = loadTemplate('plan-checker');
110
+ return interpolate(template, {
111
+ plan_content: planContent,
112
+ phase_requirements: phaseRequirements,
113
+ context_decisions: contextDecisions,
114
+ phase_goal: phaseGoal
115
+ });
116
+ }
117
+
118
+ /**
119
+ * Run the plan command.
120
+ *
121
+ * Default: Generate planner instructions for current phase.
122
+ * --phase N: Target specific phase.
123
+ * --all: Plan all unplanned phases sequentially.
124
+ * --check: Generate plan-checker instructions for existing plans.
125
+ *
126
+ * @param {string[]} args - CLI arguments
127
+ * @param {object} [opts] - Options (brainDir for testing)
128
+ * @returns {object} Structured result
129
+ */
130
+ async function run(args = [], opts = {}) {
131
+ const brainDir = opts.brainDir || path.join(process.cwd(), '.brain');
132
+ const state = readState(brainDir);
133
+
134
+ if (!state) {
135
+ error("No brain state found. Run 'brain-dev init' first.");
136
+ return { error: 'no-state' };
137
+ }
138
+
139
+ const isAll = args.includes('--all');
140
+ const isCheck = args.includes('--check');
141
+ const isAdvocate = args.includes('--advocate');
142
+
143
+ if (isAll) {
144
+ return handleAll(brainDir, state);
145
+ }
146
+
147
+ if (isCheck) {
148
+ return handleCheck(args, brainDir, state);
149
+ }
150
+
151
+ if (isAdvocate) {
152
+ return handleAdvocate(args, brainDir, state);
153
+ }
154
+
155
+ return handleSingle(args, brainDir, state);
156
+ }
157
+
158
+ /**
159
+ * Handle single-phase planning.
160
+ */
161
+ function handleSingle(args, brainDir, state) {
162
+ const phaseIdx = args.indexOf('--phase');
163
+ const phaseNumber = phaseIdx >= 0 ? parseInt(args[phaseIdx + 1], 10) : state.phase.current;
164
+
165
+ const roadmap = parseRoadmap(brainDir);
166
+ const phase = roadmap.phases.find(p => p.number === phaseNumber);
167
+
168
+ if (!phase) {
169
+ error(`Phase ${phaseNumber} not found in roadmap.`);
170
+ return { error: 'phase-not-found' };
171
+ }
172
+
173
+ const { prompt, output_dir } = generatePlannerPrompt(phase, brainDir);
174
+
175
+ // Get planner agent metadata and resolve model
176
+ const plannerAgent = getAgent('planner');
177
+ const model = resolveModel('planner', state);
178
+
179
+ // Log spawn event
180
+ logEvent(brainDir, phaseNumber, {
181
+ type: 'spawn',
182
+ agent: 'planner',
183
+ plan: 'all'
184
+ });
185
+
186
+ // Append checker loop instruction
187
+ const checkerInstruction = '\n\n> After plans are generated, plan-checker will validate. Be prepared for revision requests. Max 5 checker iterations before deadlock analysis.';
188
+ const fullPrompt = prompt + checkerInstruction;
189
+
190
+ // Update state: phase status = "planning"
191
+ state.phase.status = 'planning';
192
+ writeState(brainDir, state);
193
+
194
+ const result = {
195
+ action: 'spawn-planner',
196
+ phase: phaseNumber,
197
+ prompt: fullPrompt,
198
+ output_dir,
199
+ model,
200
+ checker_enabled: true,
201
+ max_checker_iterations: 5,
202
+ nextAction: '/brain:execute'
203
+ };
204
+
205
+ if (state.workflow && state.workflow.advocate !== false && !args.includes('--no-advocate')) {
206
+ result.advocate_enabled = true;
207
+ result.advocate_instruction = 'After plan-checker passes, run Devil\'s Advocate: brain-dev plan --advocate --phase ' + phaseNumber;
208
+ }
209
+
210
+ const humanLines = [
211
+ `[brain] Planner instructions generated for Phase ${phaseNumber}: ${phase.name}`,
212
+ `[brain] Output directory: ${output_dir}`,
213
+ `[brain] Model: ${model}`,
214
+ `[brain] Checker enabled: true`,
215
+ result.advocate_enabled ? `[brain] Advocate enabled: true` : '',
216
+ '',
217
+ fullPrompt
218
+ ].filter(Boolean);
219
+ output(result, humanLines.join('\n'));
220
+
221
+ return result;
222
+ }
223
+
224
+ /**
225
+ * Handle --check flag: generate plan-checker instructions for existing plans.
226
+ */
227
+ function handleCheck(args, brainDir, state) {
228
+ const phaseIdx = args.indexOf('--phase');
229
+ const phaseNumber = phaseIdx >= 0 ? parseInt(args[phaseIdx + 1], 10) : state.phase.current;
230
+
231
+ const roadmap = parseRoadmap(brainDir);
232
+ const phase = roadmap.phases.find(p => p.number === phaseNumber);
233
+
234
+ if (!phase) {
235
+ error(`Phase ${phaseNumber} not found in roadmap.`);
236
+ return { error: 'phase-not-found' };
237
+ }
238
+
239
+ const phaseDir = findPhaseDir(brainDir, phaseNumber);
240
+ if (!phaseDir) {
241
+ error(`Phase ${phaseNumber} directory not found.`);
242
+ return { error: 'phase-dir-not-found' };
243
+ }
244
+
245
+ // Read existing plans
246
+ const files = fs.readdirSync(phaseDir);
247
+ const planFiles = files.filter(f => /^PLAN-\d+\.md$/.test(f)).sort();
248
+
249
+ if (planFiles.length === 0) {
250
+ error(`No plans found for phase ${phaseNumber}. Run '/brain:plan' first.`);
251
+ return { error: 'no-plans' };
252
+ }
253
+
254
+ // Read context for checker
255
+ const contextContent = readContext(brainDir, phaseNumber) || '_No context decisions._';
256
+ const phaseRequirements = phase.requirements.join(', ') || 'None specified';
257
+ const phaseGoal = phase.goal || 'No goal specified';
258
+
259
+ // Generate checker prompt for each plan
260
+ const planPrompts = planFiles.map(planFile => {
261
+ const planContent = fs.readFileSync(path.join(phaseDir, planFile), 'utf8');
262
+ const prompt = generateCheckerPrompt(planContent, phaseRequirements, contextContent, phaseGoal);
263
+ return { path: path.join(phaseDir, planFile), prompt };
264
+ });
265
+
266
+ // Get checker agent metadata
267
+ const checkerAgent = getAgent('plan-checker');
268
+ const model = resolveModel('plan-checker', state);
269
+
270
+ // Log event
271
+ logEvent(brainDir, phaseNumber, {
272
+ type: 'spawn',
273
+ agent: 'plan-checker',
274
+ plans: planFiles.length
275
+ });
276
+
277
+ const safetyValve = 'After 5 revision iterations, output deadlock analysis with both planner and checker perspectives for user to break tie.';
278
+
279
+ const result = {
280
+ action: 'spawn-checker',
281
+ plans: planPrompts,
282
+ max_iterations: 5,
283
+ model,
284
+ safety_valve: safetyValve
285
+ };
286
+
287
+ const humanLines = [
288
+ `[brain] Plan-checker instructions generated for Phase ${phaseNumber}`,
289
+ `[brain] Plans to check: ${planFiles.length}`,
290
+ `[brain] Model: ${model}`,
291
+ `[brain] Max iterations: 5`,
292
+ '',
293
+ safetyValve
294
+ ];
295
+ output(result, humanLines.join('\n'));
296
+
297
+ return result;
298
+ }
299
+
300
+ /**
301
+ * Infer phase type from phase name keywords.
302
+ * @param {string} phaseName
303
+ * @returns {string} 'scaffolding' | 'production' | 'standard'
304
+ */
305
+ function inferPhaseType(phaseName) {
306
+ const lower = (phaseName || '').toLowerCase();
307
+ if (lower.includes('foundation') || lower.includes('scaffold')) return 'scaffolding';
308
+ if (lower.includes('production') || lower.includes('deploy') || lower.includes('verification')) return 'production';
309
+ return 'standard';
310
+ }
311
+
312
+ /**
313
+ * Handle --advocate flag: generate Devil's Advocate stress-test instructions.
314
+ */
315
+ function handleAdvocate(args, brainDir, state) {
316
+ const phaseIdx = args.indexOf('--phase');
317
+ const phaseNumber = phaseIdx >= 0 ? parseInt(args[phaseIdx + 1], 10) : state.phase.current;
318
+
319
+ const roadmap = parseRoadmap(brainDir);
320
+ const phase = roadmap.phases.find(p => p.number === phaseNumber);
321
+
322
+ if (!phase) {
323
+ error(`Phase ${phaseNumber} not found in roadmap.`);
324
+ return { error: 'phase-not-found' };
325
+ }
326
+
327
+ const phaseDir = findPhaseDir(brainDir, phaseNumber);
328
+ if (!phaseDir) {
329
+ error(`Phase ${phaseNumber} directory not found.`);
330
+ return { error: 'phase-dir-not-found' };
331
+ }
332
+
333
+ // Read existing plans
334
+ const files = fs.readdirSync(phaseDir);
335
+ const planFiles = files.filter(f => /^PLAN-\d+\.md$/.test(f)).sort();
336
+
337
+ if (planFiles.length === 0) {
338
+ error(`No plans found for phase ${phaseNumber}. Run '/brain:plan' first.`);
339
+ return { error: 'no-plans' };
340
+ }
341
+
342
+ const phaseGoal = phase.goal || 'No goal specified';
343
+ const phaseType = inferPhaseType(phase.name);
344
+ const budget = (state.complexity && state.complexity.phase_overrides && state.complexity.phase_overrides[phaseNumber])
345
+ || getDefaultBudget(phaseType);
346
+
347
+ const advocateTemplate = loadTemplate('advocate');
348
+
349
+ const planPrompts = planFiles.map(planFile => {
350
+ const planContent = fs.readFileSync(path.join(phaseDir, planFile), 'utf8');
351
+ const { score } = estimateFromPlan(planContent);
352
+ const prompt = interpolate(advocateTemplate, {
353
+ plan_content: planContent,
354
+ phase_goal: phaseGoal,
355
+ complexity_score: String(score),
356
+ complexity_budget: String(budget)
357
+ });
358
+ return { path: path.join(phaseDir, planFile), prompt };
359
+ });
360
+
361
+ // Get plan-checker agent metadata (advocate is a mode, not a separate agent)
362
+ const checkerAgent = getAgent('plan-checker');
363
+ const model = resolveModel('plan-checker', state);
364
+ const maxIterations = checkerAgent.modes.advocate.max_iterations;
365
+
366
+ const padded = String(phaseNumber).padStart(2, '0');
367
+ const reportPath = path.join(phaseDir, `${padded}-ADVOCATE.md`);
368
+
369
+ // Log spawn event
370
+ logEvent(brainDir, phaseNumber, {
371
+ type: 'spawn',
372
+ agent: 'plan-checker',
373
+ mode: 'advocate',
374
+ plans: planFiles.length
375
+ });
376
+
377
+ const result = {
378
+ action: 'spawn-advocate',
379
+ plans: planPrompts,
380
+ max_iterations: maxIterations,
381
+ model,
382
+ report_path: reportPath
383
+ };
384
+
385
+ const humanLines = [
386
+ `[brain] Devil's Advocate instructions generated for Phase ${phaseNumber}`,
387
+ `[brain] Plans to stress-test: ${planFiles.length}`,
388
+ `[brain] Model: ${model}`,
389
+ `[brain] Max iterations: ${maxIterations}`,
390
+ `[brain] Report path: ${reportPath}`,
391
+ '',
392
+ 'Run advocate on each plan. HIGH severity findings require plan revision before execution.'
393
+ ];
394
+ output(result, humanLines.join('\n'));
395
+
396
+ return result;
397
+ }
398
+
399
+ /**
400
+ * Handle --all flag: plan all unplanned phases.
401
+ */
402
+ function handleAll(brainDir, state) {
403
+ const roadmap = parseRoadmap(brainDir);
404
+
405
+ // Find phases that are not complete and don't have existing PLAN.md files
406
+ const unplannedPhases = roadmap.phases.filter(p => {
407
+ if (p.status === 'Complete' || p.status === 'complete') return false;
408
+
409
+ // Check if phase directory has any PLAN-*.md files
410
+ const phaseDir = findPhaseDir(brainDir, p.number);
411
+ if (phaseDir) {
412
+ const files = fs.readdirSync(phaseDir);
413
+ const hasPlans = files.some(f => /^PLAN-\d+\.md$/.test(f));
414
+ if (hasPlans) return false;
415
+ }
416
+ return true;
417
+ });
418
+
419
+ const phaseInstructions = unplannedPhases.map(phase => {
420
+ const { prompt, output_dir } = generatePlannerPrompt(phase, brainDir);
421
+ return {
422
+ phase: phase.number,
423
+ name: phase.name,
424
+ prompt,
425
+ output_dir
426
+ };
427
+ });
428
+
429
+ // Update status to planning
430
+ state.phase.status = 'planning';
431
+ writeState(brainDir, state);
432
+
433
+ const result = {
434
+ action: 'spawn-planner-all',
435
+ phases: phaseInstructions,
436
+ instruction: 'Plan each phase sequentially (not in parallel). Complete one phase before starting the next.',
437
+ nextAction: '/brain:execute'
438
+ };
439
+
440
+ const humanLines = [
441
+ `[brain] Planning ${phaseInstructions.length} unplanned phase(s) sequentially:`,
442
+ ...phaseInstructions.map(p => ` - Phase ${p.phase}: ${p.name}`),
443
+ '',
444
+ 'Execute each planner sequentially. Do not parallelize.'
445
+ ];
446
+ output(result, humanLines.join('\n'));
447
+
448
+ return result;
449
+ }
450
+
451
+ module.exports = { run, generateCheckerPrompt };
@@ -0,0 +1,167 @@
1
+ 'use strict';
2
+
3
+ const path = require('node:path');
4
+ const { readState } = require('../state.cjs');
5
+ const { output, prefix, error } = require('../core.cjs');
6
+
7
+ /**
8
+ * Determine the next action based on project and phase state.
9
+ * @param {object} state - brain.json state object
10
+ * @returns {string} Next action command
11
+ */
12
+ function nextAction(state) {
13
+ // No project name -> need to create one
14
+ if (!state.project || !state.project.name) {
15
+ return '/brain:new-project';
16
+ }
17
+
18
+ const phase = state.phase || {};
19
+
20
+ // Check if all phases are done
21
+ const totalPhases = phase.total || (Array.isArray(phase.phases) ? phase.phases.length : 0);
22
+ if (phase.status === 'complete' || (totalPhases > 0 && phase.current > totalPhases)) {
23
+ return '/brain:complete';
24
+ }
25
+
26
+ // Route based on current phase status
27
+ switch (phase.status) {
28
+ case 'initialized':
29
+ return '/brain:new-project';
30
+ case 'pending':
31
+ return '/brain:discuss';
32
+ case 'discussing':
33
+ case 'discussed':
34
+ return '/brain:plan';
35
+ case 'planning':
36
+ return '/brain:execute';
37
+ case 'executing':
38
+ return '/brain:execute';
39
+ case 'executed':
40
+ return '/brain:verify';
41
+ case 'verifying':
42
+ return '/brain:verify';
43
+ case 'verified':
44
+ return '/brain:complete';
45
+ case 'verification-failed':
46
+ return '/brain:execute';
47
+ case 'complete':
48
+ return '/brain:complete';
49
+ default:
50
+ return '/brain:progress';
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Run the progress command.
56
+ * @param {string[]} args - CLI arguments
57
+ * @param {object} [opts] - Options (brainDir for testing)
58
+ * @returns {object} Structured result for piping/testing
59
+ */
60
+ async function run(args = [], opts = {}) {
61
+ const brainDir = opts.brainDir || path.join(process.cwd(), '.brain');
62
+ const state = readState(brainDir);
63
+
64
+ if (!state) {
65
+ error("No brain state found. Run 'brain-dev init' first.");
66
+ return { error: 'no-state' };
67
+ }
68
+
69
+ const verbose = args.includes('--verbose');
70
+ const phase = state.phase || {};
71
+ const phases = phase.phases || [];
72
+ const session = state.session || {};
73
+ const action = nextAction(state);
74
+
75
+ // Context warning heuristic
76
+ const estimatedUsage = session.estimatedUsage || 0;
77
+ const contextWarning = !session.contextWarningShown && estimatedUsage > 0.3;
78
+
79
+ if (verbose) {
80
+ // Full dashboard with all phases
81
+ const phaseDetails = phases.map((p, i) => {
82
+ // Handle both string ("Phase 1: Name") and object ({number, name, status}) formats
83
+ if (typeof p === 'string') {
84
+ const match = p.match(/Phase (\d+): (.+)/);
85
+ return {
86
+ number: match ? parseInt(match[1], 10) : i + 1,
87
+ name: match ? match[2] : p,
88
+ status: 'Pending',
89
+ plansTotal: 0,
90
+ plansDone: 0,
91
+ completion: 0
92
+ };
93
+ }
94
+ return {
95
+ number: p.number || i + 1,
96
+ name: p.name || 'Unknown',
97
+ status: p.status || 'Pending',
98
+ plansTotal: p.plans ? p.plans.total : 0,
99
+ plansDone: p.plans ? p.plans.done : 0,
100
+ completion: p.plans && p.plans.total > 0
101
+ ? Math.round((p.plans.done / p.plans.total) * 100)
102
+ : 0
103
+ };
104
+ });
105
+
106
+ // Human-readable table
107
+ const lines = [
108
+ prefix(`Project: ${state.project?.name || 'unnamed'}`),
109
+ prefix(`Phase ${phase.current} of ${phases.length}: ${phase.status}`),
110
+ prefix(`Next: ${action}`),
111
+ '',
112
+ ' # Name Status Plans Done',
113
+ ' - ---- ------ ----- ----'
114
+ ];
115
+
116
+ for (const p of phaseDetails) {
117
+ const num = String(p.number || '?').padEnd(2);
118
+ const name = String(p.name || '').padEnd(15);
119
+ const status = String(p.status || 'Pending').padEnd(11);
120
+ const plans = String(p.plansTotal || 0).padEnd(9);
121
+ const pct = `${p.completion || 0}%`;
122
+ lines.push(` ${num} ${name} ${status} ${plans} ${pct}`);
123
+ }
124
+
125
+ if (contextWarning) {
126
+ lines.push('');
127
+ lines.push(prefix('Warning: Context window usage is above 30%. Consider pausing with /brain:pause.'));
128
+ }
129
+
130
+ const result = {
131
+ verbose: true,
132
+ phase: { current: phase.current, total: phases.length, status: phase.status },
133
+ phases: phaseDetails,
134
+ nextAction: action,
135
+ contextWarning
136
+ };
137
+
138
+ output(result, lines.join('\n'));
139
+ return result;
140
+ }
141
+
142
+ // Short mode (default)
143
+ const totalPhases = phases.length || '?';
144
+ const currentPlan = phase.currentPlan || '?';
145
+ const totalPlans = phase.totalPlans || '?';
146
+
147
+ const lines = [
148
+ prefix(`Phase ${phase.current} of ${totalPhases}: ${phase.status} (Plan ${currentPlan} of ${totalPlans})`),
149
+ prefix(`Next: ${action}`)
150
+ ];
151
+
152
+ if (contextWarning) {
153
+ lines.push(prefix('Warning: Context window usage is above 30%. Consider pausing with /brain:pause.'));
154
+ }
155
+
156
+ const result = {
157
+ phase: { current: phase.current, total: totalPhases, status: phase.status },
158
+ status: phase.status,
159
+ nextAction: action,
160
+ contextWarning
161
+ };
162
+
163
+ output(result, lines.join('\n'));
164
+ return result;
165
+ }
166
+
167
+ module.exports = { run, nextAction };