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,504 @@
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 { loadTemplate, interpolate } = require('../templates.cjs');
7
+ const { getAgent, resolveModel } = require('../agents.cjs');
8
+ const { logEvent } = require('../logger.cjs');
9
+ const { output, error, success } = require('../core.cjs');
10
+ const antiPatterns = require('../anti-patterns.cjs');
11
+ const { buildDebuggerSpawnInstructions } = require('./execute.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}
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
+ * Parse YAML-ish frontmatter from a plan file to extract must_haves.
30
+ * This is a lightweight parser -- not a full YAML parser.
31
+ * @param {string} content - Plan file content
32
+ * @returns {object} { truths: string[], artifacts: object[], key_links: object[] }
33
+ */
34
+ function extractMustHaves(content) {
35
+ const result = { truths: [], artifacts: [], key_links: [] };
36
+
37
+ // Extract truths
38
+ const truthsMatch = content.match(/truths:\s*\n((?:\s+-\s+"[^"]*"\n?)*)/);
39
+ if (truthsMatch) {
40
+ const lines = truthsMatch[1].match(/"([^"]*)"/g);
41
+ if (lines) {
42
+ result.truths = lines.map(l => l.replace(/"/g, ''));
43
+ }
44
+ }
45
+
46
+ // Extract artifacts
47
+ const artifactsMatch = content.match(/artifacts:\s*\n((?:\s+-\s+(?:path|provides|exports).*\n?)*)/);
48
+ if (artifactsMatch) {
49
+ const paths = artifactsMatch[1].match(/path:\s*"([^"]*)"/g);
50
+ if (paths) {
51
+ result.artifacts = paths.map(p => ({
52
+ path: p.replace(/path:\s*"([^"]*)"/, '$1')
53
+ }));
54
+ }
55
+ }
56
+
57
+ // Extract key_links
58
+ const keyLinksMatch = content.match(/key_links:\s*\n((?:\s+-\s+(?:from|to|via|pattern).*\n?)*)/);
59
+ if (keyLinksMatch) {
60
+ const froms = keyLinksMatch[1].match(/from:\s*"([^"]*)"/g);
61
+ if (froms) {
62
+ result.key_links = froms.map(f => ({
63
+ from: f.replace(/from:\s*"([^"]*)"/, '$1')
64
+ }));
65
+ }
66
+ }
67
+
68
+ return result;
69
+ }
70
+
71
+ /**
72
+ * Extract phase-modified file paths from PLAN and SUMMARY frontmatter.
73
+ * @param {string[]} planFiles - Full paths to plan files
74
+ * @param {string[]} summaryFiles - Full paths to summary files
75
+ * @returns {string[]} Deduplicated array of file paths
76
+ */
77
+ function extractPhaseFiles(planFiles, summaryFiles) {
78
+ const files = new Set();
79
+
80
+ for (const planPath of planFiles) {
81
+ const content = fs.readFileSync(planPath, 'utf8');
82
+ // Extract files_modified from frontmatter
83
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
84
+ if (fmMatch) {
85
+ const fm = fmMatch[1];
86
+ const fmIdx = fm.indexOf('files_modified:');
87
+ if (fmIdx >= 0) {
88
+ const afterFm = fm.slice(fmIdx + 'files_modified:'.length);
89
+ const lines = afterFm.split('\n');
90
+ for (const line of lines) {
91
+ const itemMatch = line.match(/^\s+-\s+(.+)/);
92
+ if (itemMatch) {
93
+ files.add(itemMatch[1].trim());
94
+ } else if (line.trim() && !line.match(/^\s+-/) && !line.match(/^\s*$/)) {
95
+ break; // Hit next YAML key
96
+ }
97
+ }
98
+ }
99
+ }
100
+ }
101
+
102
+ for (const summaryPath of summaryFiles) {
103
+ let content;
104
+ try {
105
+ content = fs.readFileSync(summaryPath, 'utf8');
106
+ } catch {
107
+ continue;
108
+ }
109
+ // Extract key-files section from SUMMARY frontmatter
110
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
111
+ if (fmMatch) {
112
+ const fm = fmMatch[1];
113
+ const kfIdx = fm.indexOf('key-files:');
114
+ if (kfIdx >= 0) {
115
+ const afterKf = fm.slice(kfIdx + 'key-files:'.length);
116
+ const lines = afterKf.split('\n');
117
+ for (const line of lines) {
118
+ const itemMatch = line.match(/^\s+-\s+(.+)/);
119
+ if (itemMatch) {
120
+ files.add(itemMatch[1].trim().replace(/^["']|["']$/g, ''));
121
+ } else if (line.trim() && !line.match(/^\s+-/) && !line.match(/^\s*$/) && !line.match(/^\s+\w+:/)) {
122
+ break;
123
+ }
124
+ }
125
+ }
126
+ }
127
+ }
128
+
129
+ return [...files];
130
+ }
131
+
132
+ /**
133
+ * Format anti-pattern scan results for template interpolation.
134
+ * @param {{ findings: object[], blockers: object[], warnings: object[] }} results
135
+ * @returns {string} Formatted markdown string
136
+ */
137
+ function formatAntiPatternResults(results) {
138
+ if (results.findings.length === 0) return 'No anti-patterns detected.';
139
+ const lines = ['### Anti-Pattern Scan Results', ''];
140
+ lines.push(`**Blockers:** ${results.blockers.length}`);
141
+ lines.push(`**Warnings:** ${results.warnings.length}`);
142
+ lines.push('');
143
+ for (const f of results.findings) {
144
+ lines.push(`- [${f.severity}] ${f.name} in ${f.file}:${f.line}`);
145
+ }
146
+ return lines.join('\n');
147
+ }
148
+
149
+ /**
150
+ * Run the verify command.
151
+ *
152
+ * Default: Generate verifier instructions for current phase.
153
+ * --phase N: Target specific phase.
154
+ * --save-results <json>: Save verification results and update state.
155
+ *
156
+ * @param {string[]} args - CLI arguments
157
+ * @param {object} [opts] - Options (brainDir for testing)
158
+ * @returns {object} Structured result
159
+ */
160
+ async function run(args = [], opts = {}) {
161
+ const brainDir = opts.brainDir || path.join(process.cwd(), '.brain');
162
+ const state = readState(brainDir);
163
+
164
+ if (!state) {
165
+ error("No brain state found. Run 'brain-dev init' first.");
166
+ return { error: 'no-state' };
167
+ }
168
+
169
+ // Check for --log-recovery mode
170
+ const logRecoveryIdx = args.indexOf('--log-recovery');
171
+ if (logRecoveryIdx >= 0) {
172
+ return handleLogRecovery(args, logRecoveryIdx, brainDir, state);
173
+ }
174
+
175
+ // Check for --save-results mode
176
+ const saveIdx = args.indexOf('--save-results');
177
+ if (saveIdx >= 0) {
178
+ return handleSaveResults(args, saveIdx, brainDir, state);
179
+ }
180
+
181
+ // Parse --auto-recover flag
182
+ const autoRecover = args.includes('--auto-recover');
183
+
184
+ // Determine phase
185
+ const phaseIdx = args.indexOf('--phase');
186
+ const phaseNumber = phaseIdx >= 0
187
+ ? parseInt(args[phaseIdx + 1], 10)
188
+ : state.phase.current;
189
+
190
+ // Find phase directory
191
+ const phaseDir = findPhaseDir(brainDir, phaseNumber);
192
+ if (!phaseDir) {
193
+ error(`Phase ${phaseNumber} directory not found.`);
194
+ return { error: 'phase-not-found' };
195
+ }
196
+
197
+ // Scan for PLAN-*.md files and extract must_haves from each
198
+ const files = fs.readdirSync(phaseDir);
199
+ const planFiles = files.filter(f => /^PLAN-\d+\.md$/.test(f)).sort();
200
+
201
+ const combinedMustHaves = { truths: [], artifacts: [], key_links: [] };
202
+
203
+ for (const planFile of planFiles) {
204
+ const content = fs.readFileSync(path.join(phaseDir, planFile), 'utf8');
205
+ const mustHaves = extractMustHaves(content);
206
+ combinedMustHaves.truths.push(...mustHaves.truths);
207
+ combinedMustHaves.artifacts.push(...mustHaves.artifacts);
208
+ combinedMustHaves.key_links.push(...mustHaves.key_links);
209
+ }
210
+
211
+ // Scan for SUMMARY-*.md files for cross-referencing
212
+ const summaryFiles = files.filter(f => /^SUMMARY-\d+\.md$/.test(f)).sort();
213
+ const summaryPaths = summaryFiles.map(f => path.join(phaseDir, f));
214
+
215
+ // Extract phase-modified files for anti-pattern scanning
216
+ const planFullPaths = planFiles.map(f => path.join(phaseDir, f));
217
+ const phaseFiles = extractPhaseFiles(planFullPaths, summaryPaths);
218
+
219
+ // Run anti-pattern scan on phase files
220
+ const projectDir = path.dirname(brainDir);
221
+ const apResults = antiPatterns.scanFiles(projectDir, { files: phaseFiles });
222
+
223
+ // Build Nyquist section based on state config
224
+ const nyquistEnabled = state.workflow?.nyquist_validation ?? false;
225
+ const nyquistInstructions = [
226
+ 'For each must_have truth, search test files for a matching test:',
227
+ '- Extract key words from the truth statement (ignore stop words: can, the, a, is, are, and, or, to, in, for, with)',
228
+ '- Search test file describe/it/test blocks for 60%+ keyword overlap',
229
+ '- Report coverage ratio: N truths with matching tests / M total truths'
230
+ ].join('\n');
231
+ const nyquistSection = nyquistEnabled ? nyquistInstructions : 'Nyquist validation not enabled.';
232
+
233
+ // Build verification output path
234
+ const outputPath = path.join(phaseDir, 'VERIFICATION.md');
235
+
236
+ // Get verifier agent metadata and resolve model
237
+ const verifierAgent = getAgent('verifier');
238
+ const model = resolveModel('verifier', state);
239
+
240
+ // Log spawn event
241
+ logEvent(brainDir, phaseNumber, {
242
+ type: 'spawn',
243
+ agent: 'verifier',
244
+ truths: combinedMustHaves.truths.length,
245
+ artifacts: combinedMustHaves.artifacts.length,
246
+ key_links: combinedMustHaves.key_links.length
247
+ });
248
+
249
+ // Load verifier template and interpolate
250
+ const template = loadTemplate('verifier');
251
+ const mustHavesFormatted = [
252
+ '**Truths:**',
253
+ ...combinedMustHaves.truths.map(t => `- ${t}`),
254
+ '',
255
+ '**Artifacts:**',
256
+ ...combinedMustHaves.artifacts.map(a => `- ${a.path}`),
257
+ '',
258
+ '**Key Links:**',
259
+ ...combinedMustHaves.key_links.map(k => `- ${k.from}`)
260
+ ].join('\n');
261
+
262
+ const prompt = interpolate(template, {
263
+ must_haves: mustHavesFormatted,
264
+ output_path: outputPath,
265
+ anti_pattern_results: formatAntiPatternResults(apResults),
266
+ nyquist_section: nyquistSection
267
+ });
268
+
269
+ // Read depth config from state (defaults to 'deep' for backward compatibility)
270
+ const depthConfig = state.depth || 'deep';
271
+ const depthLevelCount = depthConfig === 'shallow' ? 1 : depthConfig === 'standard' ? 2 : 3;
272
+
273
+ const depthLevelDescriptions = [
274
+ '- **Level 1 (Exists):** File exists and is non-empty',
275
+ '- **Level 2 (Substantive):** Contains real implementation, no stubs or TODO-only content',
276
+ '- **Level 3 (Wired):** Properly imported and consumed by dependent modules'
277
+ ];
278
+
279
+ // Append depth and scoring instructions
280
+ const depthInstruction = [
281
+ '',
282
+ `## Depth Levels (configured: ${depthConfig})`,
283
+ '',
284
+ `Apply ${depthLevelCount} verification depth level${depthLevelCount > 1 ? 's' : ''} to each artifact:`,
285
+ ...depthLevelDescriptions.slice(0, depthLevelCount),
286
+ '',
287
+ '## Scoring',
288
+ '',
289
+ 'Report results as: must_haves_verified / must_haves_total',
290
+ 'Status levels: passed (100%), gaps_found (<100%), human_needed (automated passed, human gate pending)'
291
+ ].join('\n');
292
+
293
+ // Append SUMMARY.md cross-reference paths
294
+ const summarySection = summaryPaths.length > 0
295
+ ? '\n\n## SUMMARY.md Cross-References\n\n' + summaryPaths.map(s => `- ${s}`).join('\n')
296
+ : '';
297
+
298
+ const fullPrompt = prompt + depthInstruction + summarySection;
299
+
300
+ // Update state to verifying
301
+ state.phase.status = 'verifying';
302
+ writeState(brainDir, state);
303
+
304
+ const result = {
305
+ action: 'verify-phase',
306
+ phase: phaseNumber,
307
+ must_haves: combinedMustHaves,
308
+ output_path: outputPath,
309
+ prompt: fullPrompt,
310
+ model,
311
+ depth_levels: depthLevelCount,
312
+ depth_config: depthConfig,
313
+ scoring: true,
314
+ summary_paths: summaryPaths,
315
+ anti_pattern_scan: apResults
316
+ };
317
+
318
+ // Add recovery instructions when --auto-recover is set
319
+ if (autoRecover) {
320
+ const recoveryInstructions = buildRecoveryInstructions(phaseNumber, brainDir, state);
321
+ result.recovery = {
322
+ enabled: true,
323
+ max_cycles: 3,
324
+ instructions: recoveryInstructions,
325
+ buildDebuggerForGap: (gapSlug, errorCtx, taskCtx) =>
326
+ buildDebuggerSpawnInstructions(gapSlug, errorCtx, taskCtx, '', state)
327
+ };
328
+
329
+ // Log recovery config event
330
+ logEvent(brainDir, phaseNumber, {
331
+ type: 'recovery_config',
332
+ auto_recover: true,
333
+ max_cycles: 3
334
+ });
335
+ }
336
+
337
+ const humanLines = [
338
+ `[brain] Verifier instructions generated for Phase ${phaseNumber}`,
339
+ `[brain] Truths: ${combinedMustHaves.truths.length}`,
340
+ `[brain] Artifacts: ${combinedMustHaves.artifacts.length}`,
341
+ `[brain] Model: ${model}`,
342
+ `[brain] Depth: ${depthConfig} (${depthLevelCount} level${depthLevelCount > 1 ? 's' : ''})`,
343
+ `[brain] Output: ${outputPath}`,
344
+ '',
345
+ fullPrompt
346
+ ];
347
+ output(result, humanLines.join('\n'));
348
+
349
+ return result;
350
+ }
351
+
352
+ /**
353
+ * Handle --save-results: write VERIFICATION.md and update state.
354
+ */
355
+ function handleSaveResults(args, saveIdx, brainDir, state) {
356
+ const resultsJson = args[saveIdx + 1];
357
+ if (!resultsJson) {
358
+ error('--save-results requires a JSON argument');
359
+ return { error: 'missing-argument' };
360
+ }
361
+
362
+ let results;
363
+ try {
364
+ results = JSON.parse(resultsJson);
365
+ } catch {
366
+ error('Invalid JSON for --save-results');
367
+ return { error: 'invalid-json' };
368
+ }
369
+ if (!results || typeof results !== 'object') {
370
+ error('--save-results requires a JSON object');
371
+ return { error: 'invalid-json' };
372
+ }
373
+
374
+ // Determine phase dir
375
+ const phaseNumber = state.phase.current;
376
+ const phaseDir = findPhaseDir(brainDir, phaseNumber);
377
+
378
+ if (phaseDir) {
379
+ // Write VERIFICATION.md
380
+ const lines = [
381
+ '# Verification Results',
382
+ '',
383
+ `Phase: ${phaseNumber}`,
384
+ `Overall: ${results.passed ? 'PASSED' : 'FAILED'}`,
385
+ '',
386
+ '## Checks',
387
+ ''
388
+ ];
389
+
390
+ if (Array.isArray(results.checks)) {
391
+ for (const check of results.checks) {
392
+ if (!check || typeof check !== 'object') continue;
393
+ lines.push(`- [${check.passed ? 'x' : ' '}] ${check.name || 'unknown'} (${check.level || '?'})`);
394
+ }
395
+ }
396
+
397
+ lines.push('');
398
+ atomicWriteSync(path.join(phaseDir, 'VERIFICATION.md'), lines.join('\n'));
399
+ }
400
+
401
+ // Update state
402
+ state.phase.status = results.passed ? 'verified' : 'verification-failed';
403
+ writeState(brainDir, state);
404
+
405
+ const msg = results.passed
406
+ ? 'Verification passed! Run /brain:complete to finalize.'
407
+ : 'Verification failed. Fix gaps and re-verify.';
408
+
409
+ const nextAction = results.passed ? '/brain:complete' : '/brain:execute';
410
+ output({ action: 'results-saved', passed: results.passed, nextAction }, `[brain] ${msg}`);
411
+ return { action: 'results-saved', passed: results.passed, nextAction };
412
+ }
413
+
414
+ /**
415
+ * Build recovery protocol instructions for the verifier agent.
416
+ * These instructions tell the orchestrator how to handle gaps with debugger agents.
417
+ *
418
+ * @param {number} phaseNumber
419
+ * @param {string} brainDir
420
+ * @param {object} state
421
+ * @returns {string} Markdown instruction block
422
+ */
423
+ function buildRecoveryInstructions(phaseNumber, brainDir, state) {
424
+ return [
425
+ '## Recovery Protocol',
426
+ '',
427
+ 'When verification finds gaps, enter autonomous recovery mode:',
428
+ '',
429
+ '### Recovery Loop (max 3 cycles)',
430
+ '',
431
+ 'For each cycle (1 to 3):',
432
+ '1. Identify all unresolved gaps from verification results',
433
+ '2. For each gap, spawn a debugger agent using `brain-dev execute --spot-check` context:',
434
+ ' - Pass the gap description as error context',
435
+ ' - Pass the must_have truth/artifact as task context',
436
+ ' - The debugger will diagnose and fix the issue',
437
+ '3. After all debuggers complete, re-verify: `brain-dev verify --phase ' + phaseNumber + ' --save-results`',
438
+ '4. Log progress: `brain-dev verify --log-recovery \'{"type":"re_verify","cycle":N,"passed":false,"remaining_gaps":M}\'`',
439
+ '',
440
+ '### Non-Convergence Detection',
441
+ '',
442
+ 'Track gap count between cycles. If the gap count is not decreasing between',
443
+ 'cycle N and cycle N+1 (gaps stay same or increase), trigger early escalation',
444
+ 'after cycle 2 instead of waiting for cycle 3.',
445
+ '',
446
+ '### Escalation',
447
+ '',
448
+ 'After 3 failed cycles (or early escalation on non-convergence):',
449
+ '- Output remaining gaps with descriptions',
450
+ '- Suggest: "Run `/brain:plan --gaps` to create targeted fix plans for remaining gaps"',
451
+ '- Log: `brain-dev verify --log-recovery \'{"type":"recovery_escalated","cycles":N,"remaining_gaps":["gap-slug"]}\'`',
452
+ '',
453
+ '### JSONL Logging',
454
+ '',
455
+ 'Log each recovery event via CLI:',
456
+ '- `brain-dev verify --log-recovery \'{"type":"recovery_start","cycle":1,"gaps":N}\'`',
457
+ '- `brain-dev verify --log-recovery \'{"type":"debugger_spawn","cycle":1,"gap":"gap-slug"}\'`',
458
+ '- `brain-dev verify --log-recovery \'{"type":"debugger_result","cycle":1,"gap":"gap-slug","status":"resolved"}\'`',
459
+ '- `brain-dev verify --log-recovery \'{"type":"re_verify","cycle":1,"passed":false,"remaining_gaps":N}\'`',
460
+ '- `brain-dev verify --log-recovery \'{"type":"recovery_complete","cycles":N,"final_status":"passed"}\'`'
461
+ ].join('\n');
462
+ }
463
+
464
+ /**
465
+ * Handle --log-recovery: log a recovery event to phase JSONL.
466
+ *
467
+ * @param {string[]} args
468
+ * @param {number} logRecoveryIdx
469
+ * @param {string} brainDir
470
+ * @param {object} state
471
+ * @returns {object}
472
+ */
473
+ function handleLogRecovery(args, logRecoveryIdx, brainDir, state) {
474
+ const eventJson = args[logRecoveryIdx + 1];
475
+ if (!eventJson) {
476
+ error('--log-recovery requires a JSON argument');
477
+ return { error: 'missing-argument' };
478
+ }
479
+
480
+ let event;
481
+ try {
482
+ event = JSON.parse(eventJson);
483
+ } catch {
484
+ error('Invalid JSON for --log-recovery');
485
+ return { error: 'invalid-json' };
486
+ }
487
+
488
+ // Determine phase
489
+ const phaseIdx = args.indexOf('--phase');
490
+ const phaseNumber = phaseIdx >= 0
491
+ ? parseInt(args[phaseIdx + 1], 10)
492
+ : state.phase.current;
493
+
494
+ logEvent(brainDir, phaseNumber, event);
495
+
496
+ if (!event || typeof event !== 'object') {
497
+ error('--log-recovery requires a JSON object, not null/primitive');
498
+ return { error: 'invalid-json' };
499
+ }
500
+ output({ action: 'recovery-logged', event }, `[brain] Recovery event logged: ${event.type || 'unknown'}`);
501
+ return { action: 'recovery-logged', event };
502
+ }
503
+
504
+ module.exports = { run };