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.
- package/bin/lib/agents.cjs +16 -2
- package/bin/lib/commands/complete.cjs +4 -4
- package/bin/lib/commands/discuss.cjs +3 -3
- package/bin/lib/commands/execute.cjs +3 -3
- package/bin/lib/commands/plan.cjs +3 -3
- package/bin/lib/commands/review.cjs +4 -4
- package/bin/lib/commands/story.cjs +216 -29
- package/bin/lib/commands/verify.cjs +3 -5
- package/bin/lib/config.cjs +6 -2
- package/bin/lib/recovery.cjs +4 -5
- package/bin/lib/state.cjs +22 -0
- package/bin/lib/story-helpers.cjs +69 -0
- package/bin/templates/requirements-generator.md +115 -0
- package/bin/templates/roadmap-architect.md +104 -0
- package/package.json +1 -1
package/bin/lib/agents.cjs
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Agent registry for brain orchestration.
|
|
5
|
-
* Defines the
|
|
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
|
+
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
|
|
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
|
|
182
|
+
syncPhaseStatus(state, 'pending');
|
|
183
183
|
} else {
|
|
184
|
-
state
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
541
|
-
const
|
|
542
|
-
|
|
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,
|
|
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
|
|
662
|
+
prefix('Step: Spawning Requirements Generator Agent'),
|
|
663
|
+
storyMeta.requirementsRetries ? prefix(`Retry: ${storyMeta.requirementsRetries}/2`) : '',
|
|
557
664
|
'',
|
|
558
|
-
prefix(
|
|
559
|
-
|
|
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(
|
|
562
|
-
prefix(
|
|
563
|
-
|
|
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-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
package/bin/lib/config.cjs
CHANGED
|
@@ -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
|
};
|
package/bin/lib/recovery.cjs
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
+
```
|