brain-dev 2.4.0 → 2.5.1

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.
@@ -2,11 +2,11 @@
2
2
 
3
3
  /**
4
4
  * Agent registry for brain orchestration.
5
- * Defines the 7 core agents and their metadata.
5
+ * Defines the 11 core agents and their metadata.
6
6
  * Constant registry with discovery and validation functions.
7
7
  */
8
8
 
9
- const MAX_AGENTS = 9;
9
+ const MAX_AGENTS = 11;
10
10
 
11
11
  const AGENTS = {
12
12
  researcher: {
@@ -73,6 +73,20 @@ const AGENTS = {
73
73
  model: 'inherit',
74
74
  description: 'Maps codebase across focus areas producing structured Markdown documentation',
75
75
  focus: ['tech', 'arch', 'quality', 'concerns']
76
+ },
77
+ 'requirements-generator': {
78
+ template: 'requirements-generator',
79
+ inputs: ['project_md', 'summary_content', 'stack_expertise', 'codebase_context'],
80
+ outputs: ['REQUIREMENTS.md'],
81
+ model: 'inherit',
82
+ description: 'Generates structured requirements with acceptance criteria from research findings and project context'
83
+ },
84
+ 'roadmap-architect': {
85
+ template: 'roadmap-architect',
86
+ inputs: ['requirements_content', 'summary_content', 'project_md', 'stack_expertise'],
87
+ outputs: ['ROADMAP.md'],
88
+ model: 'inherit',
89
+ description: 'Creates strategically-phased roadmap with technical dependency analysis from requirements and research'
76
90
  }
77
91
  };
78
92
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  const fs = require('node:fs');
4
4
  const path = require('node:path');
5
- const { readState, writeState } = require('../state.cjs');
5
+ const { readState, writeState, syncPhaseStatus } = require('../state.cjs');
6
6
  const { parseRoadmap, writeRoadmap } = require('../roadmap.cjs');
7
7
  const { gitTag } = require('../git.cjs');
8
8
  const { output, error, success } = require('../core.cjs');
@@ -163,7 +163,7 @@ function handlePhaseComplete(args, phaseIdx, brainDir, state) {
163
163
  return { error: 'not-verified', nextAction: '/brain:verify' };
164
164
  }
165
165
 
166
- // Mark phase as complete in state (handle both string and object formats)
166
+ // Mark current phase as complete in per-phase array
167
167
  if (Array.isArray(state.phase.phases)) {
168
168
  const idx = state.phase.phases.findIndex(p =>
169
169
  typeof p === 'object' ? p.number === phaseNumber : false
@@ -179,9 +179,9 @@ function handlePhaseComplete(args, phaseIdx, brainDir, state) {
179
179
  const hasNextPhase = totalPhases > 0 && nextPhase <= totalPhases;
180
180
  if (hasNextPhase) {
181
181
  state.phase.current = nextPhase;
182
- state.phase.status = 'pending';
182
+ syncPhaseStatus(state, 'pending');
183
183
  } else {
184
- state.phase.status = 'complete';
184
+ syncPhaseStatus(state, 'complete');
185
185
  }
186
186
  writeState(brainDir, state);
187
187
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  const fs = require('node:fs');
4
4
  const path = require('node:path');
5
- const { readState, writeState, atomicWriteSync } = require('../state.cjs');
5
+ const { readState, writeState, atomicWriteSync, syncPhaseStatus } = require('../state.cjs');
6
6
  const { parseRoadmap } = require('../roadmap.cjs');
7
7
  const { loadTemplate, interpolate } = require('../templates.cjs');
8
8
  const { output, error, success, prefix, pipelineGate } = require('../core.cjs');
@@ -132,7 +132,7 @@ function handleAnalyze(args, brainDir, state) {
132
132
  });
133
133
 
134
134
  // Update status to discussing
135
- state.phase.status = 'discussing';
135
+ syncPhaseStatus(state, 'discussing');
136
136
  writeState(brainDir, state);
137
137
 
138
138
  const result = {
@@ -220,7 +220,7 @@ function handleSave(args, brainDir, state) {
220
220
  atomicWriteSync(contextPath, lines.join('\n'));
221
221
 
222
222
  // Update state: set phase status to "discussed"
223
- state.phase.status = 'discussed';
223
+ syncPhaseStatus(state, 'discussed');
224
224
  writeState(brainDir, state);
225
225
 
226
226
  const result = {
@@ -2,7 +2,7 @@
2
2
 
3
3
  const fs = require('node:fs');
4
4
  const path = require('node:path');
5
- const { readState, writeState } = require('../state.cjs');
5
+ const { readState, writeState, syncPhaseStatus } = require('../state.cjs');
6
6
  const { loadTemplate, interpolate, loadTemplateWithOverlay } = require('../templates.cjs');
7
7
  const { getAgent, resolveModel } = require('../agents.cjs');
8
8
  const { logEvent } = require('../logger.cjs');
@@ -304,7 +304,7 @@ async function run(args = [], opts = {}) {
304
304
 
305
305
  // All plans executed
306
306
  if (!targetPlan) {
307
- state.phase.status = 'executed';
307
+ syncPhaseStatus(state, 'executed');
308
308
  writeState(brainDir, state);
309
309
  const msg = "All plans executed. Run /brain:review before verify.";
310
310
  output({ action: 'all-executed', message: msg, nextAction: '/brain:review' }, `[brain] ${msg}\n${pipelineGate('npx brain-dev review --phase ' + phaseNumber)}`);
@@ -385,7 +385,7 @@ async function run(args = [], opts = {}) {
385
385
  } catch { /* cost tracking failure is non-fatal */ }
386
386
 
387
387
  // Update state to executing
388
- state.phase.status = 'executing';
388
+ syncPhaseStatus(state, 'executing');
389
389
  if (!state.phase.execution_started_at) {
390
390
  state.phase.execution_started_at = new Date().toISOString();
391
391
  }
@@ -2,7 +2,7 @@
2
2
 
3
3
  const fs = require('node:fs');
4
4
  const path = require('node:path');
5
- const { readState, writeState } = require('../state.cjs');
5
+ const { readState, writeState, syncPhaseStatus } = require('../state.cjs');
6
6
  const { parseRoadmap } = require('../roadmap.cjs');
7
7
  const { loadTemplate, interpolate, loadTemplateWithOverlay } = require('../templates.cjs');
8
8
  const { getAgent, resolveModel } = require('../agents.cjs');
@@ -291,7 +291,7 @@ function handleSingle(args, brainDir, state) {
291
291
  const fullPrompt = prompt + checkerInstruction;
292
292
 
293
293
  // Update state: phase status = "planning"
294
- state.phase.status = 'planning';
294
+ syncPhaseStatus(state, 'planning');
295
295
  writeState(brainDir, state);
296
296
 
297
297
  const result = {
@@ -531,7 +531,7 @@ function handleAll(brainDir, state) {
531
531
  });
532
532
 
533
533
  // Update status to planning
534
- state.phase.status = 'planning';
534
+ syncPhaseStatus(state, 'planning');
535
535
  writeState(brainDir, state);
536
536
 
537
537
  const result = {
@@ -2,7 +2,7 @@
2
2
 
3
3
  const fs = require('node:fs');
4
4
  const path = require('node:path');
5
- const { readState, writeState } = require('../state.cjs');
5
+ const { readState, writeState, syncPhaseStatus } = require('../state.cjs');
6
6
  const { loadTemplateWithOverlay, interpolate } = require('../templates.cjs');
7
7
  const { getAgent, resolveModel } = require('../agents.cjs');
8
8
  const { logEvent } = require('../logger.cjs');
@@ -93,7 +93,7 @@ async function run(args = [], opts = {}) {
93
93
  });
94
94
 
95
95
  // Update state
96
- state.phase.status = 'reviewing';
96
+ syncPhaseStatus(state, 'reviewing');
97
97
  writeState(brainDir, state);
98
98
 
99
99
  const result = {
@@ -145,14 +145,14 @@ function handleResult(brainDir, state, args) {
145
145
  const reviewStatus = statusMatch ? statusMatch[1] : 'passed';
146
146
 
147
147
  if (reviewStatus === 'critical') {
148
- state.phase.status = 'review-failed';
148
+ syncPhaseStatus(state, 'review-failed');
149
149
  writeState(brainDir, state);
150
150
  error('Review found critical issues. Fix them and re-run /brain:review.');
151
151
  output({ action: 'review-failed', phase: phaseNumber, status: reviewStatus }, '');
152
152
  return { action: 'review-failed', phase: phaseNumber };
153
153
  }
154
154
 
155
- state.phase.status = 'reviewed';
155
+ syncPhaseStatus(state, 'reviewed');
156
156
  writeState(brainDir, state);
157
157
 
158
158
  const result = {
@@ -3,17 +3,18 @@
3
3
  const fs = require('node:fs');
4
4
  const path = require('node:path');
5
5
  const { parseArgs } = require('node:util');
6
- const { readState, writeState } = require('../state.cjs');
6
+ const { readState, writeState, syncPhaseStatus } = require('../state.cjs');
7
7
  const { output, error, prefix } = require('../core.cjs');
8
8
  const { logEvent } = require('../logger.cjs');
9
- const { loadTemplate, interpolate } = require('../templates.cjs');
9
+ const { loadTemplate, interpolate, loadTemplateWithOverlay } = require('../templates.cjs');
10
10
  const { getAgent, resolveModel } = require('../agents.cjs');
11
11
  const { generateExpertise } = require('../stack-expert.cjs');
12
12
  const {
13
13
  RESEARCH_AREAS, CORE_QUESTIONS, buildBrownfieldQuestions,
14
14
  readDetection, buildCodebaseContext, generateProjectMd,
15
- generateRequirementsMd, extractFeatures, extractSection
15
+ generateRequirementsMd, validateRequirementsMd, extractFeatures, extractSection
16
16
  } = require('../story-helpers.cjs');
17
+ const { getDetectedFramework } = require('../stack-expert.cjs');
17
18
 
18
19
  /**
19
20
  * Get research areas with optional framework-specific additions.
@@ -318,10 +319,55 @@ function handleContinue(brainDir, state, values) {
318
319
  }
319
320
 
320
321
  if (!hasRequirementsMd) {
322
+ if (storyMeta.status === 'requirements-generating') {
323
+ const humanLines = prefix('Requirements agent is generating... Run --continue after it finishes.');
324
+ output({ action: 'requirements-pending' }, humanLines);
325
+ return { action: 'requirements-pending' };
326
+ }
321
327
  return stepRequirements(brainDir, storyDir, storyMeta, state);
322
328
  }
323
329
 
330
+ // Validate requirements quality before proceeding to roadmap.
331
+ // Only validate agent-generated requirements (hasSummaryMd).
332
+ // Programmatic fallback (--no-research) skips validation because its output
333
+ // format intentionally lacks acceptance criteria.
334
+ if (hasRequirementsMd && !hasRoadmapMd && !storyMeta.requirementsValidated && hasSummaryMd) {
335
+ const reqContent = fs.readFileSync(path.join(storyDir, 'REQUIREMENTS.md'), 'utf8');
336
+ const validation = validateRequirementsMd(reqContent);
337
+
338
+ if (!validation.valid && (storyMeta.requirementsRetries || 0) < 2) {
339
+ fs.unlinkSync(path.join(storyDir, 'REQUIREMENTS.md'));
340
+ storyMeta.requirementsRetries = (storyMeta.requirementsRetries || 0) + 1;
341
+ storyMeta.requirementsFeedback = validation.issues;
342
+
343
+ logEvent(brainDir, 0, { type: 'requirements-retry', story: storyMeta.num, attempt: storyMeta.requirementsRetries, issues: validation.issues.length });
344
+
345
+ const humanLines = [
346
+ prefix(`Requirements quality check failed (attempt ${storyMeta.requirementsRetries}/2):`),
347
+ ...validation.issues.map(i => prefix(` - ${i}`)),
348
+ '',
349
+ prefix('Re-spawning requirements agent with feedback...')
350
+ ].join('\n');
351
+ output({ action: 'requirements-retry', issues: validation.issues, stats: validation.stats }, humanLines);
352
+
353
+ return stepRequirements(brainDir, storyDir, storyMeta, state);
354
+ }
355
+
356
+ storyMeta.requirementsValidated = true;
357
+ fs.writeFileSync(path.join(storyDir, 'story.json'), JSON.stringify(storyMeta, null, 2));
358
+
359
+ if (!validation.valid) {
360
+ const humanLines = prefix(`Warning: REQUIREMENTS.md has ${validation.issues.length} quality issues after ${storyMeta.requirementsRetries} retries. Proceeding anyway.`);
361
+ output({ action: 'requirements-quality-warning', issues: validation.issues, stats: validation.stats }, humanLines);
362
+ }
363
+ }
364
+
324
365
  if (!hasRoadmapMd) {
366
+ if (storyMeta.status === 'roadmap-generating') {
367
+ const humanLines = prefix('Roadmap agent is generating... Run --continue after it finishes.');
368
+ output({ action: 'roadmap-pending' }, humanLines);
369
+ return { action: 'roadmap-pending' };
370
+ }
325
371
  return stepRoadmap(brainDir, storyDir, storyMeta, state);
326
372
  }
327
373
 
@@ -526,9 +572,8 @@ function stepSynthesize(brainDir, storyDir, storyMeta, state) {
526
572
  // ---------------------------------------------------------------------------
527
573
 
528
574
  function stepRequirements(brainDir, storyDir, storyMeta, state) {
529
- // Read PROJECT.md for features
575
+ // Read PROJECT.md
530
576
  const projectMd = fs.readFileSync(path.join(storyDir, 'PROJECT.md'), 'utf8');
531
- const features = extractFeatures(projectMd);
532
577
 
533
578
  // Read research summary if available
534
579
  const summaryPath = path.join(storyDir, 'research', 'SUMMARY.md');
@@ -537,36 +582,107 @@ function stepRequirements(brainDir, storyDir, storyMeta, state) {
537
582
  summaryContent = fs.readFileSync(summaryPath, 'utf8');
538
583
  }
539
584
 
540
- // Generate REQUIREMENTS.md
541
- const { requirementsMd, categories } = generateRequirementsMd(features, summaryContent);
542
- fs.writeFileSync(path.join(storyDir, 'REQUIREMENTS.md'), requirementsMd, 'utf8');
585
+ // No research → use programmatic fallback
586
+ const skipResearch = storyMeta.noResearch === true || !summaryContent;
587
+ if (skipResearch) {
588
+ const features = extractFeatures(projectMd);
589
+ const { requirementsMd, categories } = generateRequirementsMd(features, summaryContent);
590
+ fs.writeFileSync(path.join(storyDir, 'REQUIREMENTS.md'), requirementsMd, 'utf8');
591
+
592
+ storyMeta.status = 'requirements';
593
+ fs.writeFileSync(path.join(storyDir, 'story.json'), JSON.stringify(storyMeta, null, 2));
594
+
595
+ const activeIdx = (state.stories.active || []).findIndex(s => s.dirName === storyMeta.dirName);
596
+ if (activeIdx >= 0) state.stories.active[activeIdx].status = 'requirements';
597
+ writeState(brainDir, state);
598
+
599
+ logEvent(brainDir, 0, { type: 'story-requirements', story: storyMeta.num, categories: categories.length });
600
+
601
+ const humanLines = [
602
+ prefix(`Story: ${storyMeta.version} ${storyMeta.title}`),
603
+ prefix('Step: Requirements Generated (programmatic fallback)'),
604
+ '',
605
+ prefix(`REQUIREMENTS.md written with ${categories.length} categories:`),
606
+ ...categories.map(c => prefix(` - ${c.name} (${c.items.length} requirements)`)),
607
+ '',
608
+ prefix('Review REQUIREMENTS.md and confirm.'),
609
+ prefix('Then run: brain-dev story --continue')
610
+ ].join('\n');
611
+
612
+ const result = {
613
+ action: 'requirements-generated',
614
+ content: requirementsMd,
615
+ categories,
616
+ instruction: 'Review and confirm'
617
+ };
618
+
619
+ output(result, humanLines);
620
+ return result;
621
+ }
622
+
623
+ // Research exists → spawn requirements-generator agent
624
+ const outputPath = path.join(storyDir, 'REQUIREMENTS.md');
625
+ const framework = getDetectedFramework(brainDir);
626
+ const template = loadTemplateWithOverlay('requirements-generator', framework);
627
+ const stackExpertise = generateExpertise(brainDir, 'general');
628
+ const detection = readDetection(brainDir);
629
+ const codebaseContext = detection ? buildCodebaseContext(detection) : '';
630
+
631
+ // Build feedback section if retrying
632
+ let feedbackSection = '';
633
+ if (storyMeta.requirementsFeedback && storyMeta.requirementsFeedback.length > 0) {
634
+ feedbackSection = '\n\n## PREVIOUS ATTEMPT FEEDBACK\n\n' +
635
+ 'Your previous output was rejected for these quality issues:\n' +
636
+ storyMeta.requirementsFeedback.map(i => `- ${i}`).join('\n') +
637
+ '\n\nFix ALL these issues in your next attempt.';
638
+ }
639
+
640
+ const prompt = interpolate(template, {
641
+ project_md: projectMd,
642
+ summary_content: summaryContent,
643
+ stack_expertise: stackExpertise || '',
644
+ codebase_context: codebaseContext || '_No codebase context available._',
645
+ output_path: outputPath
646
+ }) + feedbackSection;
647
+
648
+ const model = resolveModel('requirements-generator', state);
543
649
 
544
650
  // Update status
545
- storyMeta.status = 'requirements';
651
+ storyMeta.status = 'requirements-generating';
546
652
  fs.writeFileSync(path.join(storyDir, 'story.json'), JSON.stringify(storyMeta, null, 2));
547
653
 
548
654
  const activeIdx = (state.stories.active || []).findIndex(s => s.dirName === storyMeta.dirName);
549
- if (activeIdx >= 0) state.stories.active[activeIdx].status = 'requirements';
655
+ if (activeIdx >= 0) state.stories.active[activeIdx].status = 'requirements-generating';
550
656
  writeState(brainDir, state);
551
657
 
552
- logEvent(brainDir, 0, { type: 'story-requirements', story: storyMeta.num, categories: categories.length });
658
+ logEvent(brainDir, 0, { type: 'story-requirements-spawn', story: storyMeta.num, retry: storyMeta.requirementsRetries || 0 });
553
659
 
554
660
  const humanLines = [
555
661
  prefix(`Story: ${storyMeta.version} ${storyMeta.title}`),
556
- prefix('Step: Requirements Generated'),
662
+ prefix('Step: Spawning Requirements Generator Agent'),
663
+ storyMeta.requirementsRetries ? prefix(`Retry: ${storyMeta.requirementsRetries}/2`) : '',
557
664
  '',
558
- prefix(`REQUIREMENTS.md written with ${categories.length} categories:`),
559
- ...categories.map(c => prefix(` - ${c.name} (${c.items.length} requirements)`)),
665
+ prefix('The agent will read your research summary and project context'),
666
+ prefix('to generate detailed, specific requirements with acceptance criteria.'),
560
667
  '',
561
- prefix('Review REQUIREMENTS.md and confirm.'),
562
- prefix('Then run: brain-dev story --continue')
563
- ].join('\n');
668
+ prefix(`Output: ${outputPath}`),
669
+ prefix(`Model: ${model}`),
670
+ '',
671
+ 'IMPORTANT: Use the Agent tool to spawn the requirements-generator agent.',
672
+ 'Do NOT generate requirements yourself.',
673
+ '',
674
+ 'After agent completes, run: brain-dev story --continue',
675
+ '',
676
+ prompt
677
+ ].filter(Boolean).join('\n');
564
678
 
565
679
  const result = {
566
- action: 'requirements-generated',
567
- content: requirementsMd,
568
- categories,
569
- instruction: 'Review and confirm'
680
+ action: 'spawn-requirements-agent',
681
+ prompt,
682
+ model,
683
+ outputPath,
684
+ retry: storyMeta.requirementsRetries || 0,
685
+ instruction: 'Spawn requirements-generator agent, then run --continue'
570
686
  };
571
687
 
572
688
  output(result, humanLines);
@@ -582,6 +698,82 @@ function stepRoadmap(brainDir, storyDir, storyMeta, state) {
582
698
  const reqPath = path.join(storyDir, 'REQUIREMENTS.md');
583
699
  const reqContent = fs.readFileSync(reqPath, 'utf8');
584
700
 
701
+ // Read research summary if available
702
+ const summaryPath = path.join(storyDir, 'research', 'SUMMARY.md');
703
+ let summaryContent = '';
704
+ if (fs.existsSync(summaryPath)) {
705
+ summaryContent = fs.readFileSync(summaryPath, 'utf8');
706
+ }
707
+
708
+ // No research → use programmatic fallback
709
+ const skipResearch = storyMeta.noResearch === true || !summaryContent;
710
+ if (skipResearch) {
711
+ return stepRoadmapProgrammatic(brainDir, storyDir, storyMeta, state, reqContent);
712
+ }
713
+
714
+ // Research exists → spawn roadmap-architect agent
715
+ const projectMd = fs.readFileSync(path.join(storyDir, 'PROJECT.md'), 'utf8');
716
+ const outputPath = path.join(storyDir, 'ROADMAP.md');
717
+ const framework = getDetectedFramework(brainDir);
718
+ const template = loadTemplateWithOverlay('roadmap-architect', framework);
719
+ const stackExpertise = generateExpertise(brainDir, 'general');
720
+
721
+ const prompt = interpolate(template, {
722
+ requirements_content: reqContent,
723
+ summary_content: summaryContent,
724
+ project_md: projectMd,
725
+ stack_expertise: stackExpertise || '',
726
+ output_path: outputPath,
727
+ story_version: storyMeta.version || '',
728
+ story_title: storyMeta.title || ''
729
+ });
730
+
731
+ const model = resolveModel('roadmap-architect', state);
732
+
733
+ // Update status
734
+ storyMeta.status = 'roadmap-generating';
735
+ fs.writeFileSync(path.join(storyDir, 'story.json'), JSON.stringify(storyMeta, null, 2));
736
+
737
+ const activeIdx = (state.stories.active || []).findIndex(s => s.dirName === storyMeta.dirName);
738
+ if (activeIdx >= 0) state.stories.active[activeIdx].status = 'roadmap-generating';
739
+ writeState(brainDir, state);
740
+
741
+ logEvent(brainDir, 0, { type: 'story-roadmap-spawn', story: storyMeta.num });
742
+
743
+ const humanLines = [
744
+ prefix(`Story: ${storyMeta.version} ${storyMeta.title}`),
745
+ prefix('Step: Spawning Roadmap Architect Agent'),
746
+ '',
747
+ prefix('The agent will analyze requirements and research findings'),
748
+ prefix('to create a strategically-phased roadmap with real dependencies.'),
749
+ '',
750
+ prefix(`Output: ${outputPath}`),
751
+ prefix(`Model: ${model}`),
752
+ '',
753
+ 'IMPORTANT: Use the Agent tool to spawn the roadmap-architect agent.',
754
+ 'Do NOT generate the roadmap yourself.',
755
+ '',
756
+ 'After agent completes, run: brain-dev story --continue',
757
+ '',
758
+ prompt
759
+ ].join('\n');
760
+
761
+ const result = {
762
+ action: 'spawn-roadmap-agent',
763
+ prompt,
764
+ model,
765
+ outputPath,
766
+ instruction: 'Spawn roadmap-architect agent, then run --continue'
767
+ };
768
+
769
+ output(result, humanLines);
770
+ return result;
771
+ }
772
+
773
+ /**
774
+ * Programmatic fallback for roadmap generation (no research).
775
+ */
776
+ function stepRoadmapProgrammatic(brainDir, storyDir, storyMeta, state, reqContent) {
585
777
  // Parse categories from REQUIREMENTS.md headings
586
778
  const categoryRegex = /## (.+)\n([\s\S]*?)(?=\n## |$)/g;
587
779
  const phases = [];
@@ -591,8 +783,8 @@ function stepRoadmap(brainDir, storyDir, storyMeta, state) {
591
783
  while ((catMatch = categoryRegex.exec(reqContent)) !== null) {
592
784
  const catName = catMatch[1].trim();
593
785
  if (catName === 'Requirements' || catName.toLowerCase() === 'requirements') continue;
786
+ if (catName === 'Traceability' || catName.toLowerCase() === 'traceability') continue;
594
787
 
595
- // Extract requirement IDs from this category
596
788
  const catBody = catMatch[2];
597
789
  const reqIds = [];
598
790
  const reqLineRegex = /- \*\*(\d+\.\d+)\*\*/g;
@@ -601,7 +793,6 @@ function stepRoadmap(brainDir, storyDir, storyMeta, state) {
601
793
  reqIds.push(reqMatch[1]);
602
794
  }
603
795
 
604
- // Dependencies: each phase depends on the previous one (except phase 1)
605
796
  const dependsOn = phaseNum > 1 ? [phaseNum - 1] : [];
606
797
 
607
798
  phases.push({
@@ -617,7 +808,6 @@ function stepRoadmap(brainDir, storyDir, storyMeta, state) {
617
808
  phaseNum++;
618
809
  }
619
810
 
620
- // Fallback: if no phases extracted, create a single phase
621
811
  if (phases.length === 0) {
622
812
  phases.push({
623
813
  number: 1,
@@ -630,7 +820,6 @@ function stepRoadmap(brainDir, storyDir, storyMeta, state) {
630
820
  });
631
821
  }
632
822
 
633
- // Generate ROADMAP.md
634
823
  const roadmapLines = [
635
824
  '# Roadmap',
636
825
  '',
@@ -650,7 +839,6 @@ function stepRoadmap(brainDir, storyDir, storyMeta, state) {
650
839
  roadmapLines.push('');
651
840
  }
652
841
 
653
- // Progress table
654
842
  roadmapLines.push('## Progress');
655
843
  roadmapLines.push('');
656
844
  roadmapLines.push('| Phase | Status | Plans |');
@@ -663,7 +851,6 @@ function stepRoadmap(brainDir, storyDir, storyMeta, state) {
663
851
  const roadmapMd = roadmapLines.join('\n');
664
852
  fs.writeFileSync(path.join(storyDir, 'ROADMAP.md'), roadmapMd, 'utf8');
665
853
 
666
- // Update status
667
854
  storyMeta.status = 'roadmap';
668
855
  fs.writeFileSync(path.join(storyDir, 'story.json'), JSON.stringify(storyMeta, null, 2));
669
856
 
@@ -675,7 +862,7 @@ function stepRoadmap(brainDir, storyDir, storyMeta, state) {
675
862
 
676
863
  const humanLines = [
677
864
  prefix(`Story: ${storyMeta.version} ${storyMeta.title}`),
678
- prefix('Step: Roadmap Generated'),
865
+ prefix('Step: Roadmap Generated (programmatic fallback)'),
679
866
  '',
680
867
  prefix(`ROADMAP.md written with ${phases.length} phases:`),
681
868
  ...phases.map(p => prefix(` Phase ${p.number}: ${p.name} (depends on: ${p.dependsOn.length === 0 ? 'none' : p.dependsOn.join(', ')})`)),
@@ -726,7 +913,7 @@ function stepActivate(brainDir, state, storyDir, storyMeta) {
726
913
  // Update state phases
727
914
  state.phase = state.phase || { current: 0, status: 'initialized', total: 0, phases: [] };
728
915
  state.phase.current = 1;
729
- state.phase.status = 'ready';
916
+ syncPhaseStatus(state, 'ready');
730
917
  state.phase.total = roadmapData.phases.length;
731
918
  state.phase.phases = roadmapData.phases.map(p => ({
732
919
  number: p.number,
@@ -2,7 +2,7 @@
2
2
 
3
3
  const fs = require('node:fs');
4
4
  const path = require('node:path');
5
- const { readState, writeState, atomicWriteSync } = require('../state.cjs');
5
+ const { readState, writeState, atomicWriteSync, syncPhaseStatus } = require('../state.cjs');
6
6
  const { loadTemplate, interpolate, loadTemplateWithOverlay } = require('../templates.cjs');
7
7
  const { getAgent, resolveModel } = require('../agents.cjs');
8
8
  const { logEvent } = require('../logger.cjs');
@@ -320,7 +320,7 @@ async function run(args = [], opts = {}) {
320
320
  const fullPrompt = prompt + depthInstruction + summarySection;
321
321
 
322
322
  // Update state to verifying
323
- state.phase.status = 'verifying';
323
+ syncPhaseStatus(state, 'verifying');
324
324
  writeState(brainDir, state);
325
325
 
326
326
  const result = {
@@ -421,9 +421,7 @@ function handleSaveResults(args, saveIdx, brainDir, state) {
421
421
  }
422
422
 
423
423
  // Update state
424
- state.phase.status = results.passed ? 'verified' : 'verification-failed';
425
- // Clear execution timer after verification completes
426
- state.phase.execution_started_at = null;
424
+ syncPhaseStatus(state, results.passed ? 'verified' : 'verification-failed');
427
425
  writeState(brainDir, state);
428
426
 
429
427
  const msg = results.passed
@@ -56,7 +56,9 @@ const PROFILES = {
56
56
  models: {
57
57
  'plan-checker': 'claude-sonnet-4-20250514',
58
58
  verifier: 'claude-sonnet-4-20250514',
59
- researcher: 'claude-sonnet-4-20250514'
59
+ researcher: 'claude-sonnet-4-20250514',
60
+ 'requirements-generator': 'claude-sonnet-4-20250514',
61
+ 'roadmap-architect': 'claude-sonnet-4-20250514'
60
62
  }
61
63
  },
62
64
  budget: {
@@ -67,7 +69,9 @@ const PROFILES = {
67
69
  'plan-checker': 'claude-haiku-4-20250514',
68
70
  verifier: 'claude-haiku-4-20250514',
69
71
  debugger: 'claude-haiku-4-20250514',
70
- mapper: 'claude-haiku-4-20250514'
72
+ mapper: 'claude-haiku-4-20250514',
73
+ 'requirements-generator': 'claude-sonnet-4-20250514',
74
+ 'roadmap-architect': 'claude-haiku-4-20250514'
71
75
  }
72
76
  }
73
77
  };
@@ -4,7 +4,7 @@ const fs = require('node:fs');
4
4
  const path = require('node:path');
5
5
  const { readLock, isLockStale, clearStaleLock } = require('./lock.cjs');
6
6
  const { readLog } = require('./logger.cjs');
7
- const { readState, writeState, VALID_PHASE_STATUSES } = require('./state.cjs');
7
+ const { readState, writeState, VALID_PHASE_STATUSES, syncPhaseStatus } = require('./state.cjs');
8
8
 
9
9
  /**
10
10
  * Analyze the JSONL execution log for a crashed phase.
@@ -293,11 +293,11 @@ function autoResume(brainDir, logAnalysis, state) {
293
293
  if (state.phase) {
294
294
  if (state.phase.status === 'executing' && logAnalysis.inProgressTask) {
295
295
  // There was work in progress - set to planned so execution can retry
296
- state.phase.status = 'planned';
296
+ syncPhaseStatus(state, 'planned');
297
297
  changes.push(`Reset phase status from "executing" to "planned"`);
298
298
  } else if (state.phase.status === 'executing' && !logAnalysis.inProgressTask) {
299
299
  // All tasks were completed or none in progress
300
- state.phase.status = 'planned';
300
+ syncPhaseStatus(state, 'planned');
301
301
  changes.push(`Reset phase status to "planned" for re-verification`);
302
302
  }
303
303
 
@@ -337,8 +337,7 @@ function rollback(brainDir, staleLock, state) {
337
337
  // Reset phase status
338
338
  if (state.phase) {
339
339
  const previousStatus = state.phase.status;
340
- state.phase.status = 'planned';
341
- state.phase.execution_started_at = null;
340
+ syncPhaseStatus(state, 'planned');
342
341
  changes.push(`Reset phase status from "${previousStatus}" to "planned"`);
343
342
  }
344
343
 
package/bin/lib/state.cjs CHANGED
@@ -517,6 +517,27 @@ const VALID_PHASE_STATUSES = [
517
517
  'partial', 'failed', 'paused', 'complete'
518
518
  ];
519
519
 
520
+ /**
521
+ * Atomically sync phase status across both top-level and per-phase array.
522
+ * Also clears execution_started_at when regressing to pre-execute states.
523
+ * @param {object} state - brain.json state object
524
+ * @param {string} newStatus - New status value
525
+ */
526
+ function syncPhaseStatus(state, newStatus) {
527
+ const preExecuteStatuses = ['pending', 'ready', 'discussing', 'discussed', 'planning', 'planned'];
528
+ if (preExecuteStatuses.includes(newStatus)) {
529
+ state.phase.execution_started_at = null;
530
+ }
531
+
532
+ state.phase.status = newStatus;
533
+ if (Array.isArray(state.phase.phases) && state.phase.current > 0) {
534
+ const idx = state.phase.phases.findIndex(p => p.number === state.phase.current);
535
+ if (idx >= 0) {
536
+ state.phase.phases[idx].status = newStatus;
537
+ }
538
+ }
539
+ }
540
+
520
541
  module.exports = {
521
542
  atomicWriteSync,
522
543
  readState,
@@ -524,5 +545,6 @@ module.exports = {
524
545
  generateStateMd,
525
546
  createDefaultState,
526
547
  migrateState,
548
+ syncPhaseStatus,
527
549
  VALID_PHASE_STATUSES
528
550
  };
@@ -426,6 +426,74 @@ function extractSection(content, heading) {
426
426
  return match ? match[1].trim() : '';
427
427
  }
428
428
 
429
+ /**
430
+ * Validate REQUIREMENTS.md quality after LLM generation.
431
+ * Checks for acceptance criteria, description length, banned phrases, and structure.
432
+ * @param {string} content - REQUIREMENTS.md content
433
+ * @returns {{ valid: boolean, issues: string[], stats: { requirements: number, withAcceptance: number, categories: number, acceptanceRate: number } }}
434
+ */
435
+ function validateRequirementsMd(content) {
436
+ const issues = [];
437
+
438
+ // Parse requirements (checkbox format: - [ ] **N.M**: Description)
439
+ const reqRegex = /- \[[ x]\] \*\*(\S+)\*\*:?\s*(.+)/g;
440
+ let reqCount = 0;
441
+ let withAcceptance = 0;
442
+ let match;
443
+
444
+ while ((match = reqRegex.exec(content)) !== null) {
445
+ reqCount++;
446
+ const reqId = match[1];
447
+ const reqText = match[2];
448
+
449
+ // Check for acceptance criterion following this requirement
450
+ const reqIdx = content.indexOf(match[0]);
451
+ const nextLines = content.slice(reqIdx + match[0].length).split('\n').slice(0, 4);
452
+ const hasAcceptance = nextLines.some(l =>
453
+ l.trim().startsWith('- Acceptance:') || l.trim().startsWith('Acceptance:'));
454
+
455
+ if (hasAcceptance) withAcceptance++;
456
+ else issues.push(`${reqId}: missing acceptance criterion`);
457
+
458
+ // Check description length
459
+ if (reqText.split(/\s+/).length < 5) {
460
+ issues.push(`${reqId}: description too short (< 5 words)`);
461
+ }
462
+
463
+ // Check for banned vague phrases
464
+ const banned = [
465
+ 'implement properly', 'handle correctly', 'make it work',
466
+ 'ensure quality', 'integrate well', 'setup recommended'
467
+ ];
468
+ for (const phrase of banned) {
469
+ if (reqText.toLowerCase().includes(phrase)) {
470
+ issues.push(`${reqId}: contains banned vague phrase "${phrase}"`);
471
+ }
472
+ }
473
+ }
474
+
475
+ // Check category count (exclude special sections like Traceability, Requirements)
476
+ const allHeadings = content.match(/^## [A-Z].*/gm) || [];
477
+ const catCount = allHeadings.filter(h =>
478
+ !h.match(/^## (Traceability|Requirements|REQUIREMENTS)/)).length;
479
+ if (catCount < 2) issues.push('Too few categories (expected at least 2)');
480
+
481
+ // Check minimum requirement count
482
+ if (reqCount < 3) issues.push(`Only ${reqCount} requirements found (expected at least 3)`);
483
+
484
+ const valid = issues.length === 0;
485
+ return {
486
+ valid,
487
+ issues,
488
+ stats: {
489
+ requirements: reqCount,
490
+ withAcceptance,
491
+ categories: catCount,
492
+ acceptanceRate: reqCount > 0 ? Math.round(withAcceptance / reqCount * 100) : 0
493
+ }
494
+ };
495
+ }
496
+
429
497
  module.exports = {
430
498
  RESEARCH_AREAS,
431
499
  CORE_QUESTIONS,
@@ -434,6 +502,7 @@ module.exports = {
434
502
  buildCodebaseContext,
435
503
  generateProjectMd,
436
504
  generateRequirementsMd,
505
+ validateRequirementsMd,
437
506
  extractFeatures,
438
507
  extractSection
439
508
  };
@@ -0,0 +1,115 @@
1
+ # Requirements Generator Agent
2
+
3
+ You are a requirements engineering specialist. Your job is to analyze research findings and project context to produce specific, measurable, testable requirements.
4
+
5
+ ## Technology Context
6
+ {{stack_expertise}}
7
+
8
+ ## Project Context
9
+ {{project_md}}
10
+
11
+ ## Research Summary
12
+ {{summary_content}}
13
+
14
+ ## Codebase Context
15
+ {{codebase_context}}
16
+
17
+ ## Your Task
18
+
19
+ Generate a structured REQUIREMENTS.md by analyzing the research summary and project context above. Every requirement must be **grounded in research findings** — no generic filler.
20
+
21
+ ## Output Format
22
+
23
+ Write the file to: `{{output_path}}`
24
+
25
+ Use this EXACT format:
26
+
27
+ ```markdown
28
+ # Requirements
29
+
30
+ ## [Category Name]
31
+
32
+ Goal: [1-sentence goal grounded in specific research findings]
33
+
34
+ - [ ] **N.M**: [Specific, measurable requirement — minimum 5 words]
35
+ - Acceptance: [Testable criterion that can become an assert/expect statement]
36
+ - Source: [Which research area identified this need]
37
+ - Priority: P0|P1|P2
38
+
39
+ ## Traceability
40
+
41
+ | Requirement | Category | Priority | Source |
42
+ |-------------|----------|----------|--------|
43
+ | N.M | Category | P0/P1/P2 | Research area |
44
+ ```
45
+
46
+ ### Category Rules
47
+ - Derive categories from ACTUAL research themes and project structure
48
+ - Do NOT use hardcoded generic names like "Infrastructure" or "Quality & Security"
49
+ - Each category needs a Goal line grounded in specific research findings
50
+ - Minimum 2 categories, maximum 8
51
+
52
+ ### Requirement Rules
53
+ 1. **Checkbox format required**: `- [ ] **N.M**: Description`
54
+ - N = category number (1, 2, 3...)
55
+ - M = requirement within category (1, 2, 3...)
56
+ 2. **Every requirement MUST have an Acceptance line** — testable criterion
57
+ 3. **Description minimum 5 words** — be specific about WHAT, not just "implement X"
58
+ 4. **Source must reference research** — which research finding drives this requirement
59
+ 5. **Priority**: P0 = must have, P1 = should have, P2 = nice to have
60
+
61
+ ### Banned Vague Phrases (DO NOT USE)
62
+ - "implement properly"
63
+ - "handle correctly"
64
+ - "make it work"
65
+ - "ensure quality"
66
+ - "integrate well"
67
+ - "setup recommended technology stack"
68
+ - "implement testing strategy"
69
+ - "implement security requirements"
70
+ - "implement recommended architecture patterns"
71
+
72
+ ### Bad vs Good Examples
73
+
74
+ **BAD:**
75
+ ```
76
+ - [ ] **1.1**: Setup recommended technology stack
77
+ - [ ] **2.1**: Implement security requirements
78
+ - [ ] **3.1**: Handle errors properly
79
+ ```
80
+
81
+ **GOOD:**
82
+ ```
83
+ - [ ] **1.1**: User authentication via JWT with RS256 signing, 24h token expiration
84
+ - Acceptance: POST /auth/login with valid credentials returns 200 with JWT containing user_id and exp claims; expired tokens return 401
85
+ - Source: Security research — session management recommendations
86
+ - Priority: P0
87
+
88
+ - [ ] **1.2**: PostgreSQL schema with users table including email uniqueness constraint and bcrypt password hashing
89
+ - Acceptance: Migration creates users table with UNIQUE index on email column; passwords stored as bcrypt hashes with cost factor 12
90
+ - Source: Stack research — database recommendations
91
+ - Priority: P0
92
+
93
+ - [ ] **2.1**: Rate limiting on authentication endpoints at 5 requests per minute per IP
94
+ - Acceptance: 6th login attempt within 60 seconds returns 429 with Retry-After header
95
+ - Source: Security research — brute force protection
96
+ - Priority: P1
97
+ ```
98
+
99
+ ### Cross-Cutting Concerns
100
+ Include requirements from ALL research areas, not just features:
101
+ - Security findings → security requirements
102
+ - Performance findings → performance requirements
103
+ - Testing findings → testing requirements
104
+ - Architecture findings → structural requirements
105
+ - Pitfalls findings → defensive requirements
106
+
107
+ ## Output Marker
108
+
109
+ When complete, output:
110
+ ```
111
+ ## REQUIREMENTS COMPLETE
112
+ Categories: [count]
113
+ Requirements: [total count]
114
+ P0: [count] | P1: [count] | P2: [count]
115
+ ```
@@ -0,0 +1,104 @@
1
+ # Roadmap Architect Agent
2
+
3
+ You are a technical roadmap architect. Your job is to create a strategically-phased development plan with REAL technical dependencies — not a linear chain of categories.
4
+
5
+ ## Technology Context
6
+ {{stack_expertise}}
7
+
8
+ ## Requirements
9
+ {{requirements_content}}
10
+
11
+ ## Research Summary
12
+ {{summary_content}}
13
+
14
+ ## Project Context
15
+ {{project_md}}
16
+
17
+ ## Your Task
18
+
19
+ Analyze requirements and research to create a phased ROADMAP.md with technical dependency analysis, risk assessment, and strategic sequencing.
20
+
21
+ ## Dependency Analysis Rules
22
+
23
+ 1. **Identify TECHNICAL dependencies** between requirements:
24
+ - Database schema must exist before CRUD endpoints
25
+ - Auth infrastructure before protected routes
26
+ - Shared models/types before their consumers
27
+ - Configuration/environment setup before feature code
28
+ - Core utilities before features that use them
29
+
30
+ 2. **Group by TECHNICAL AFFINITY**, not by requirement category:
31
+ - Requirements sharing the same domain model belong together
32
+ - Infrastructure requirements precede feature requirements
33
+ - Integration requirements follow their component requirements
34
+ - Testing setup can parallel with early implementation
35
+
36
+ 3. **Dependency graph must be a DAG**:
37
+ - No circular dependencies
38
+ - Phase N can only depend on phases < N
39
+ - Multiple phases CAN share dependencies (fan-in)
40
+ - A phase CAN enable multiple later phases (fan-out)
41
+ - NOT all phases need to be sequential — identify parallelizable work
42
+
43
+ ## Output Format
44
+
45
+ Write the file to: `{{output_path}}`
46
+
47
+ Use this EXACT format:
48
+
49
+ ```markdown
50
+ # Roadmap
51
+
52
+ Story: {{story_version}} {{story_title}}
53
+
54
+ ## Phases
55
+
56
+ ### Phase N: [Descriptive Name — NOT category name]
57
+
58
+ - **Goal:** [Specific goal grounded in requirements — NOT "Implement X requirements"]
59
+ - **Depends on:** [Comma-separated phase numbers based on TECHNICAL analysis, or None]
60
+ - **Requirements:** [Comma-separated requirement IDs from REQUIREMENTS.md]
61
+ - **Risk:** [Primary risk from research for this phase]
62
+ - **Estimated complexity:** Low|Medium|High
63
+ - **Status:** Pending
64
+
65
+ ## Progress
66
+
67
+ | Phase | Status | Plans |
68
+ |-------|--------|-------|
69
+ | N | Pending | 0/0 |
70
+
71
+ ## Phase Rationale
72
+
73
+ [2-3 sentences explaining WHY phases are ordered this way, citing specific technical dependencies discovered in research]
74
+ ```
75
+
76
+ ## Phase Goal Anti-Patterns (DO NOT USE)
77
+
78
+ - "Implement core features requirements" — circular, says nothing
79
+ - "Setup infrastructure" — too vague
80
+ - "Implement X requirements" — restates the category name
81
+ - "Build the system" — meaningless
82
+
83
+ ## Good Phase Goal Examples
84
+
85
+ - "Establish database schema, authentication flow, and API foundation that all feature phases depend on"
86
+ - "Build real-time notification pipeline using WebSocket connections with Redis pub/sub backing"
87
+ - "Add rate limiting, input validation, and CSRF protection across all public endpoints"
88
+
89
+ ## Phase Sizing Guidelines
90
+
91
+ - Each phase should have 3-8 requirements
92
+ - If a phase has 10+ requirements, it's too large — split it
93
+ - If a phase has 1-2 requirements, merge with a related phase
94
+ - Aim for 3-6 phases total for most projects
95
+
96
+ ## Output Marker
97
+
98
+ When complete, output:
99
+ ```
100
+ ## ROADMAP COMPLETE
101
+ Phases: [count]
102
+ Dependency depth: [max chain length]
103
+ Parallelizable: [phases with same/no dependencies that could run concurrently]
104
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brain-dev",
3
- "version": "2.4.0",
3
+ "version": "2.5.1",
4
4
  "description": "AI-powered development workflow orchestrator",
5
5
  "author": "halilcosdu",
6
6
  "license": "MIT",