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.
- package/LICENSE +21 -0
- package/README.md +152 -0
- package/agents/brain-checker.md +33 -0
- package/agents/brain-debugger.md +35 -0
- package/agents/brain-executor.md +37 -0
- package/agents/brain-mapper.md +44 -0
- package/agents/brain-planner.md +49 -0
- package/agents/brain-researcher.md +47 -0
- package/agents/brain-synthesizer.md +43 -0
- package/agents/brain-verifier.md +41 -0
- package/bin/brain-tools.cjs +185 -0
- package/bin/lib/adr.cjs +283 -0
- package/bin/lib/agents.cjs +152 -0
- package/bin/lib/anti-patterns.cjs +183 -0
- package/bin/lib/audit.cjs +268 -0
- package/bin/lib/commands/adr.cjs +126 -0
- package/bin/lib/commands/complete.cjs +270 -0
- package/bin/lib/commands/config.cjs +306 -0
- package/bin/lib/commands/discuss.cjs +237 -0
- package/bin/lib/commands/execute.cjs +415 -0
- package/bin/lib/commands/health.cjs +103 -0
- package/bin/lib/commands/map.cjs +101 -0
- package/bin/lib/commands/new-project.cjs +885 -0
- package/bin/lib/commands/pause.cjs +142 -0
- package/bin/lib/commands/phase-manage.cjs +357 -0
- package/bin/lib/commands/plan.cjs +451 -0
- package/bin/lib/commands/progress.cjs +167 -0
- package/bin/lib/commands/quick.cjs +447 -0
- package/bin/lib/commands/resume.cjs +196 -0
- package/bin/lib/commands/storm.cjs +590 -0
- package/bin/lib/commands/verify.cjs +504 -0
- package/bin/lib/commands.cjs +263 -0
- package/bin/lib/complexity.cjs +138 -0
- package/bin/lib/complexity.test.cjs +108 -0
- package/bin/lib/config.cjs +452 -0
- package/bin/lib/core.cjs +62 -0
- package/bin/lib/detect.cjs +603 -0
- package/bin/lib/git.cjs +112 -0
- package/bin/lib/health.cjs +356 -0
- package/bin/lib/init.cjs +310 -0
- package/bin/lib/logger.cjs +100 -0
- package/bin/lib/platform.cjs +58 -0
- package/bin/lib/requirements.cjs +158 -0
- package/bin/lib/roadmap.cjs +228 -0
- package/bin/lib/security.cjs +237 -0
- package/bin/lib/state.cjs +353 -0
- package/bin/lib/templates.cjs +48 -0
- package/bin/templates/advocate.md +182 -0
- package/bin/templates/checkpoint.md +55 -0
- package/bin/templates/debugger.md +148 -0
- package/bin/templates/discuss.md +60 -0
- package/bin/templates/executor.md +201 -0
- package/bin/templates/mapper.md +129 -0
- package/bin/templates/plan-checker.md +134 -0
- package/bin/templates/planner.md +165 -0
- package/bin/templates/researcher.md +78 -0
- package/bin/templates/storm.html +376 -0
- package/bin/templates/synthesis.md +30 -0
- package/bin/templates/verifier.md +181 -0
- package/commands/brain/adr.md +34 -0
- package/commands/brain/complete.md +37 -0
- package/commands/brain/config.md +37 -0
- package/commands/brain/discuss.md +35 -0
- package/commands/brain/execute.md +38 -0
- package/commands/brain/health.md +33 -0
- package/commands/brain/map.md +35 -0
- package/commands/brain/new-project.md +38 -0
- package/commands/brain/pause.md +26 -0
- package/commands/brain/plan.md +38 -0
- package/commands/brain/progress.md +28 -0
- package/commands/brain/quick.md +51 -0
- package/commands/brain/resume.md +28 -0
- package/commands/brain/storm.md +30 -0
- package/commands/brain/verify.md +39 -0
- package/hooks/bootstrap.sh +54 -0
- package/hooks/post-tool-use.sh +45 -0
- package/hooks/statusline.sh +130 -0
- package/package.json +36 -0
|
@@ -0,0 +1,885 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { readState, writeState } = require('../state.cjs');
|
|
6
|
+
const { loadTemplate, interpolate } = require('../templates.cjs');
|
|
7
|
+
const { writeRoadmap, parseRoadmap } = require('../roadmap.cjs');
|
|
8
|
+
const { detectProject } = require('../detect.cjs');
|
|
9
|
+
const { output, error, success } = require('../core.cjs');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 6 research focus areas for new-project research phase.
|
|
13
|
+
*/
|
|
14
|
+
const RESEARCH_AREAS = [
|
|
15
|
+
{ name: 'stack-researcher', file: 'stack.md', area: 'Technology Stack & Ecosystem', description: 'technology stack, frameworks, libraries, and ecosystem tools' },
|
|
16
|
+
{ name: 'architecture-researcher', file: 'architecture.md', area: 'Architecture Patterns', description: 'architecture patterns, project structure, and design decisions' },
|
|
17
|
+
{ name: 'pitfalls-researcher', file: 'pitfalls.md', area: 'Pitfalls & Anti-patterns', description: 'common pitfalls, anti-patterns, and mistakes to avoid' },
|
|
18
|
+
{ name: 'security-researcher', file: 'security.md', area: 'Security & Compliance', description: 'security requirements, compliance needs, and vulnerability prevention' },
|
|
19
|
+
{ name: 'competitive-researcher', file: 'competitive.md', area: 'Competitive Analysis', description: 'competitive landscape, similar products, and differentiation opportunities' },
|
|
20
|
+
{ name: 'testing-researcher', file: 'testing.md', area: 'Testing Strategy', description: 'testing approach, coverage targets, and quality assurance strategy' }
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Core vision-first questions for greenfield (new) projects.
|
|
25
|
+
* Each question includes AskUserQuestion-compatible options as guidance templates.
|
|
26
|
+
*/
|
|
27
|
+
const CORE_QUESTIONS = [
|
|
28
|
+
{
|
|
29
|
+
id: 'vision',
|
|
30
|
+
text: 'What are you building? Describe the core idea in 1-2 sentences.',
|
|
31
|
+
header: 'Project Type',
|
|
32
|
+
options: [
|
|
33
|
+
{ label: 'Web Application', description: 'Full-stack web app with frontend and backend' },
|
|
34
|
+
{ label: 'API / Backend Service', description: 'REST/GraphQL API or microservice' },
|
|
35
|
+
{ label: 'CLI Tool', description: 'Command-line tool or developer utility' },
|
|
36
|
+
{ label: 'Mobile App', description: 'iOS/Android or cross-platform mobile application' }
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: 'users',
|
|
41
|
+
text: 'Who is it for? Who are the primary users?',
|
|
42
|
+
header: 'Target Users',
|
|
43
|
+
options: [
|
|
44
|
+
{ label: 'Developers', description: 'Software engineers and technical users' },
|
|
45
|
+
{ label: 'End Users', description: 'Non-technical consumers or business users' },
|
|
46
|
+
{ label: 'Internal Team', description: 'Internal tools for your organization' },
|
|
47
|
+
{ label: 'B2B Customers', description: 'Business clients and enterprise users' }
|
|
48
|
+
]
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: 'features',
|
|
52
|
+
text: 'What must it do? List the 3-5 most critical features.',
|
|
53
|
+
header: 'Core Features',
|
|
54
|
+
multiSelect: true,
|
|
55
|
+
options: [
|
|
56
|
+
{ label: 'Auth & Users', description: 'User registration, login, roles, permissions' },
|
|
57
|
+
{ label: 'CRUD & Data', description: 'Create, read, update, delete operations with database' },
|
|
58
|
+
{ label: 'Real-time', description: 'WebSocket, live updates, notifications' },
|
|
59
|
+
{ label: 'Integrations', description: 'Third-party APIs, payment, email, etc.' }
|
|
60
|
+
]
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: 'constraints',
|
|
64
|
+
text: 'Any constraints? (tech stack preferences, timeline, budget, existing codebase)',
|
|
65
|
+
header: 'Constraints',
|
|
66
|
+
options: [
|
|
67
|
+
{ label: 'Existing Codebase', description: 'Adding to or extending an existing project' },
|
|
68
|
+
{ label: 'Specific Stack', description: 'Must use a particular language/framework' },
|
|
69
|
+
{ label: 'Tight Timeline', description: 'MVP or deadline-driven development' },
|
|
70
|
+
{ label: 'No Constraints', description: 'Flexible on technology and timeline' }
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Build brownfield (existing project) questions dynamically from detection results.
|
|
77
|
+
* @param {object} detection - Detection result from detectProject()
|
|
78
|
+
* @returns {Array} AskUserQuestion-compatible question specs
|
|
79
|
+
*/
|
|
80
|
+
function buildBrownfieldQuestions(detection) {
|
|
81
|
+
const { stack, features, summary, workspace } = detection;
|
|
82
|
+
|
|
83
|
+
// Question 1: Vision/Goal - context-aware with workspace
|
|
84
|
+
let visionText = `I detected a ${summary}. What do you want to do?`;
|
|
85
|
+
if (workspace && workspace.siblings.length > 0) {
|
|
86
|
+
const sibNames = workspace.siblings.map(s => `${s.name} (${s.stack.framework || s.stack.language})`).join(', ');
|
|
87
|
+
visionText = `I detected a ${summary}. I also found ${workspace.siblings.length} related project(s): ${sibNames}. What do you want to do?`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const visionQuestion = {
|
|
91
|
+
id: 'vision',
|
|
92
|
+
text: visionText,
|
|
93
|
+
header: 'Goal',
|
|
94
|
+
options: [
|
|
95
|
+
{ label: 'Add New Feature', description: 'Build something new on top of the existing codebase' },
|
|
96
|
+
{ label: 'Refactor / Improve', description: 'Improve architecture, performance, or code quality' },
|
|
97
|
+
{ label: 'Fix & Stabilize', description: 'Fix bugs, add tests, improve reliability' },
|
|
98
|
+
{ label: 'Scale & Optimize', description: 'Performance optimization, scaling, infrastructure' }
|
|
99
|
+
]
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// Question 2: Users - same as greenfield but context-aware text
|
|
103
|
+
const usersQuestion = {
|
|
104
|
+
id: 'users',
|
|
105
|
+
text: 'Who are the current or target users of this project?',
|
|
106
|
+
header: 'Target Users',
|
|
107
|
+
options: [
|
|
108
|
+
{ label: 'Developers', description: 'Software engineers and technical users' },
|
|
109
|
+
{ label: 'End Users', description: 'Non-technical consumers or business users' },
|
|
110
|
+
{ label: 'Internal Team', description: 'Internal tools for your organization' },
|
|
111
|
+
{ label: 'B2B Customers', description: 'Business clients and enterprise users' }
|
|
112
|
+
]
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// Question 3: Features/Priorities - dynamically built from detection
|
|
116
|
+
const featureOptions = [];
|
|
117
|
+
// Add detected features as "extend" options (max 2)
|
|
118
|
+
for (const feat of features.slice(0, 2)) {
|
|
119
|
+
featureOptions.push({ label: `Extend ${feat}`, description: `Build on existing ${feat} capability` });
|
|
120
|
+
}
|
|
121
|
+
// Fill remaining slots with generic options (total max 4)
|
|
122
|
+
const genericFeatureOpts = [
|
|
123
|
+
{ label: 'New Feature', description: 'Something not yet in the codebase' },
|
|
124
|
+
{ label: 'Testing & Quality', description: 'Improve test coverage and code quality' },
|
|
125
|
+
{ label: 'Performance', description: 'Optimize speed, memory, or resource usage' },
|
|
126
|
+
{ label: 'Documentation', description: 'Improve docs, README, API documentation' }
|
|
127
|
+
];
|
|
128
|
+
for (const opt of genericFeatureOpts) {
|
|
129
|
+
if (featureOptions.length >= 4) break;
|
|
130
|
+
featureOptions.push(opt);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const featuresText = features.length > 0
|
|
134
|
+
? `I found these capabilities: ${features.join(', ')}. What's the next priority?`
|
|
135
|
+
: 'What are the most important features or improvements?';
|
|
136
|
+
|
|
137
|
+
const featuresQuestion = {
|
|
138
|
+
id: 'features',
|
|
139
|
+
text: featuresText,
|
|
140
|
+
header: 'Priorities',
|
|
141
|
+
multiSelect: true,
|
|
142
|
+
options: featureOptions
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// Question 4: Constraints - stack-aware (use primary stack)
|
|
146
|
+
const primary = stack.primary || stack;
|
|
147
|
+
const stackLabel = primary.framework || (primary.language ? primary.language.charAt(0).toUpperCase() + primary.language.slice(1) : 'current stack');
|
|
148
|
+
const constraintsText = primary.framework
|
|
149
|
+
? `The project uses ${primary.framework}${primary.language ? ` (${primary.language})` : ''}. Any additional constraints?`
|
|
150
|
+
: 'Any constraints? (timeline, budget, compatibility)';
|
|
151
|
+
|
|
152
|
+
const constraintsQuestion = {
|
|
153
|
+
id: 'constraints',
|
|
154
|
+
text: constraintsText,
|
|
155
|
+
header: 'Constraints',
|
|
156
|
+
options: [
|
|
157
|
+
{ label: `Keep ${stackLabel}`, description: 'Stay within the existing technology choices' },
|
|
158
|
+
{ label: 'Allow Migration', description: 'Open to changing frameworks or libraries' },
|
|
159
|
+
{ label: 'Tight Timeline', description: 'MVP or deadline-driven development' },
|
|
160
|
+
{ label: 'No Constraints', description: 'Flexible on approach' }
|
|
161
|
+
]
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
return [visionQuestion, usersQuestion, featuresQuestion, constraintsQuestion];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Run the new-project command.
|
|
169
|
+
* Multi-step flow driven by flags:
|
|
170
|
+
* (no flags) -> Step 1: Detect project + output questions
|
|
171
|
+
* --answers <json> -> Step 2: Parse answers, spawn researchers (+ mappers for brownfield)
|
|
172
|
+
* --synthesize -> Step 3: Spawn synthesis agent
|
|
173
|
+
* --generate-roadmap -> Step 4: Generate REQUIREMENTS.md + ROADMAP.md
|
|
174
|
+
* --finalize -> Step 5: Finalize, update state
|
|
175
|
+
*
|
|
176
|
+
* @param {string[]} args - CLI arguments
|
|
177
|
+
* @param {object} [opts] - Options (brainDir override for testing, rootDir for testing)
|
|
178
|
+
* @returns {object} Structured result for Claude to act on
|
|
179
|
+
*/
|
|
180
|
+
function run(args = [], opts = {}) {
|
|
181
|
+
const brainDir = (opts && opts.brainDir) || path.join(process.cwd(), '.brain');
|
|
182
|
+
const rootDir = (opts && opts.rootDir) || process.cwd();
|
|
183
|
+
|
|
184
|
+
// Parse flags
|
|
185
|
+
const answersIdx = args.indexOf('--answers');
|
|
186
|
+
const hasSynthesize = args.includes('--synthesize');
|
|
187
|
+
const hasGenerateRoadmap = args.includes('--generate-roadmap');
|
|
188
|
+
const hasFinalize = args.includes('--finalize');
|
|
189
|
+
|
|
190
|
+
if (hasFinalize) {
|
|
191
|
+
return stepFinalize(args, brainDir);
|
|
192
|
+
}
|
|
193
|
+
if (hasGenerateRoadmap) {
|
|
194
|
+
return stepGenerateRoadmap(brainDir);
|
|
195
|
+
}
|
|
196
|
+
if (hasSynthesize) {
|
|
197
|
+
return stepSynthesize(brainDir);
|
|
198
|
+
}
|
|
199
|
+
if (answersIdx >= 0) {
|
|
200
|
+
const answersJson = args[answersIdx + 1];
|
|
201
|
+
if (!answersJson) {
|
|
202
|
+
error('--answers requires a JSON argument. Usage: brain-dev new-project --answers \'{"vision":"..."}\'');
|
|
203
|
+
return { action: 'error', error: 'missing-argument' };
|
|
204
|
+
}
|
|
205
|
+
return stepResearch(answersJson, brainDir);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Default: Step 1 - Detect + Questions
|
|
209
|
+
return stepQuestions(brainDir, rootDir);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Step 1: Detect project type, output appropriate questions.
|
|
214
|
+
* Greenfield: generic vision-first questions.
|
|
215
|
+
* Brownfield: codebase-aware context questions.
|
|
216
|
+
*/
|
|
217
|
+
function stepQuestions(brainDir, rootDir) {
|
|
218
|
+
// Check project not already initialized
|
|
219
|
+
const state = readState(brainDir);
|
|
220
|
+
if (state && state.project && state.project.initialized === true) {
|
|
221
|
+
return {
|
|
222
|
+
action: 'error',
|
|
223
|
+
message: 'Project already initialized. Use /brain:discuss to refine.'
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Detect project type
|
|
228
|
+
const detection = detectProject(rootDir);
|
|
229
|
+
|
|
230
|
+
// Persist detection for later steps
|
|
231
|
+
fs.mkdirSync(brainDir, { recursive: true });
|
|
232
|
+
fs.writeFileSync(
|
|
233
|
+
path.join(brainDir, 'detection.json'),
|
|
234
|
+
JSON.stringify(detection, null, 2),
|
|
235
|
+
'utf8'
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
// Build questions based on detection
|
|
239
|
+
const isBrownfield = detection.type === 'brownfield';
|
|
240
|
+
const questions = isBrownfield
|
|
241
|
+
? buildBrownfieldQuestions(detection)
|
|
242
|
+
: CORE_QUESTIONS;
|
|
243
|
+
|
|
244
|
+
// Build AskUserQuestion-compatible question specs
|
|
245
|
+
const askQuestions = questions.map(q => ({
|
|
246
|
+
question: q.text,
|
|
247
|
+
header: q.header,
|
|
248
|
+
multiSelect: q.multiSelect || false,
|
|
249
|
+
options: q.options
|
|
250
|
+
}));
|
|
251
|
+
|
|
252
|
+
const detectionLine = isBrownfield
|
|
253
|
+
? `[brain] Detected existing project: ${detection.summary}`
|
|
254
|
+
: '[brain] No existing code detected — starting fresh';
|
|
255
|
+
|
|
256
|
+
const humanText = [
|
|
257
|
+
'[brain] New project setup - Step 1: Gathering your vision',
|
|
258
|
+
detectionLine,
|
|
259
|
+
'',
|
|
260
|
+
'IMPORTANT: Use the AskUserQuestion tool to ask these questions.',
|
|
261
|
+
'Do NOT ask as freetext. Use the structured questions below.',
|
|
262
|
+
'The user can select predefined options OR type custom answers via "Other".',
|
|
263
|
+
'',
|
|
264
|
+
'Ask ALL 4 questions in a SINGLE AskUserQuestion call (it supports 1-4 questions):',
|
|
265
|
+
''
|
|
266
|
+
];
|
|
267
|
+
|
|
268
|
+
for (const q of questions) {
|
|
269
|
+
humanText.push(` ${q.id} [${q.header}]: ${q.text}`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
humanText.push('');
|
|
273
|
+
humanText.push('After collecting answers, run:');
|
|
274
|
+
humanText.push('brain-dev new-project --answers \'{"vision":"...","users":"...","features":"...","constraints":"..."}\'');
|
|
275
|
+
|
|
276
|
+
const result = {
|
|
277
|
+
action: 'ask-questions',
|
|
278
|
+
projectType: detection.type,
|
|
279
|
+
detection,
|
|
280
|
+
questions: askQuestions,
|
|
281
|
+
tool: 'AskUserQuestion',
|
|
282
|
+
instruction: 'Use AskUserQuestion tool with the questions array above. Do NOT ask as freetext.'
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
output(result, humanText.join('\n'));
|
|
286
|
+
return result;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Step 2: Parse user answers, create PROJECT.md draft, output researcher spawn instructions.
|
|
291
|
+
* For brownfield projects, also spawns mapper agents and injects codebase context.
|
|
292
|
+
*/
|
|
293
|
+
function stepResearch(answersJson, brainDir) {
|
|
294
|
+
let answers;
|
|
295
|
+
try {
|
|
296
|
+
answers = JSON.parse(answersJson);
|
|
297
|
+
} catch (e) {
|
|
298
|
+
return { action: 'error', message: 'Invalid JSON for --answers: ' + e.message };
|
|
299
|
+
}
|
|
300
|
+
if (!answers || typeof answers !== 'object') {
|
|
301
|
+
return { action: 'error', message: '--answers requires a JSON object, not null/primitive' };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Read detection results
|
|
305
|
+
const detection = readDetection(brainDir);
|
|
306
|
+
const isBrownfield = detection && detection.type === 'brownfield';
|
|
307
|
+
|
|
308
|
+
// Create research directory
|
|
309
|
+
const researchDir = path.join(brainDir, 'research');
|
|
310
|
+
fs.mkdirSync(researchDir, { recursive: true });
|
|
311
|
+
|
|
312
|
+
// Write PROJECT.md draft
|
|
313
|
+
const projectMd = isBrownfield
|
|
314
|
+
? generateProjectMd(answers, detection)
|
|
315
|
+
: generateProjectMd(answers);
|
|
316
|
+
fs.writeFileSync(path.join(brainDir, 'PROJECT.md'), projectMd, 'utf8');
|
|
317
|
+
|
|
318
|
+
// Build codebase context for brownfield researcher prompts
|
|
319
|
+
const codebaseContext = isBrownfield
|
|
320
|
+
? buildCodebaseContext(detection)
|
|
321
|
+
: '';
|
|
322
|
+
|
|
323
|
+
// Generate researcher prompts
|
|
324
|
+
const researcherTemplate = loadTemplate('researcher');
|
|
325
|
+
const agents = RESEARCH_AREAS.map(area => {
|
|
326
|
+
const prompt = interpolate(researcherTemplate, {
|
|
327
|
+
focus_area: area.area,
|
|
328
|
+
project_description: answers.vision || 'TBD',
|
|
329
|
+
target_users: answers.users || 'TBD',
|
|
330
|
+
key_features: answers.features || 'TBD',
|
|
331
|
+
constraints: answers.constraints || 'None',
|
|
332
|
+
focus_area_description: area.description,
|
|
333
|
+
output_path: path.join(researchDir, area.file),
|
|
334
|
+
codebase_context: codebaseContext
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
return {
|
|
338
|
+
name: area.name,
|
|
339
|
+
file: area.file,
|
|
340
|
+
outputPath: path.join(researchDir, area.file),
|
|
341
|
+
prompt
|
|
342
|
+
};
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// Build mapper agents for brownfield
|
|
346
|
+
let mapperAgents = [];
|
|
347
|
+
if (isBrownfield) {
|
|
348
|
+
const codebaseDir = path.join(brainDir, 'codebase');
|
|
349
|
+
fs.mkdirSync(codebaseDir, { recursive: true });
|
|
350
|
+
|
|
351
|
+
try {
|
|
352
|
+
const mapperTemplate = loadTemplate('mapper');
|
|
353
|
+
const mapperFoci = ['tech', 'arch'];
|
|
354
|
+
|
|
355
|
+
mapperAgents = mapperFoci.map(focus => {
|
|
356
|
+
const prompt = interpolate(mapperTemplate, {
|
|
357
|
+
focus,
|
|
358
|
+
codebase_root: path.dirname(brainDir),
|
|
359
|
+
output_dir: codebaseDir
|
|
360
|
+
});
|
|
361
|
+
return { focus, prompt, output_dir: codebaseDir };
|
|
362
|
+
});
|
|
363
|
+
} catch {
|
|
364
|
+
// Mapper template may not exist; continue without mappers
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Build output
|
|
369
|
+
const humanText = [
|
|
370
|
+
'[brain] New project setup - Step 2: Research',
|
|
371
|
+
''
|
|
372
|
+
];
|
|
373
|
+
|
|
374
|
+
if (isBrownfield && mapperAgents.length > 0) {
|
|
375
|
+
humanText.push(`[brain] Brownfield project detected. Spawning ${mapperAgents.length} mapper agents + ${agents.length} research agents in parallel.`);
|
|
376
|
+
humanText.push('');
|
|
377
|
+
humanText.push('Mapper agents (subagent_type: "brain-mapper"):');
|
|
378
|
+
for (const ma of mapperAgents) {
|
|
379
|
+
humanText.push(` - mapper-${ma.focus} -> .brain/codebase/`);
|
|
380
|
+
}
|
|
381
|
+
humanText.push('');
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
humanText.push('PROJECT.md draft created. Now spawn research agents in parallel.');
|
|
385
|
+
humanText.push('IMPORTANT: Use the Agent tool to spawn each agent. Do NOT do the research yourself.');
|
|
386
|
+
humanText.push('');
|
|
387
|
+
for (const agent of agents) {
|
|
388
|
+
humanText.push(` - ${agent.name} -> ${agent.file}`);
|
|
389
|
+
}
|
|
390
|
+
humanText.push('');
|
|
391
|
+
humanText.push('Use the Agent tool (subagent_type: "brain-researcher") to spawn all 6 in parallel.');
|
|
392
|
+
if (isBrownfield && mapperAgents.length > 0) {
|
|
393
|
+
humanText.push('Use the Agent tool (subagent_type: "brain-mapper") to spawn mapper agents in parallel alongside researchers.');
|
|
394
|
+
}
|
|
395
|
+
humanText.push('IMPORTANT: Use subagent_type "brain-researcher" for research agents, "brain-mapper" for mapper agents.');
|
|
396
|
+
humanText.push('After all complete, run: npx brain-dev new-project --synthesize');
|
|
397
|
+
|
|
398
|
+
const action = isBrownfield && mapperAgents.length > 0
|
|
399
|
+
? 'spawn-mapper-and-researchers'
|
|
400
|
+
: 'spawn-researchers';
|
|
401
|
+
|
|
402
|
+
const result = { action, agents };
|
|
403
|
+
if (mapperAgents.length > 0) {
|
|
404
|
+
result.mapperAgents = mapperAgents;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
output(result, humanText.join('\n'));
|
|
408
|
+
return result;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Step 3: Output synthesis agent spawn instructions.
|
|
413
|
+
* For brownfield projects, includes codebase analysis in synthesis prompt.
|
|
414
|
+
*/
|
|
415
|
+
function stepSynthesize(brainDir) {
|
|
416
|
+
const researchDir = path.join(brainDir, 'research');
|
|
417
|
+
const outputPath = path.join(researchDir, 'SUMMARY.md');
|
|
418
|
+
|
|
419
|
+
const synthesisTemplate = loadTemplate('synthesis');
|
|
420
|
+
|
|
421
|
+
// Check for brownfield codebase analysis
|
|
422
|
+
const detection = readDetection(brainDir);
|
|
423
|
+
const isBrownfield = detection && detection.type === 'brownfield';
|
|
424
|
+
const codebaseDir = path.join(brainDir, 'codebase');
|
|
425
|
+
const hasCodebaseAnalysis = isBrownfield && fs.existsSync(codebaseDir);
|
|
426
|
+
|
|
427
|
+
let codebaseInstructions = '';
|
|
428
|
+
if (hasCodebaseAnalysis) {
|
|
429
|
+
codebaseInstructions = [
|
|
430
|
+
'',
|
|
431
|
+
'Also read codebase analysis from ' + codebaseDir + '/:',
|
|
432
|
+
'- STACK.md (detected technology stack)',
|
|
433
|
+
'- ARCHITECTURE.md (detected architecture patterns)',
|
|
434
|
+
'',
|
|
435
|
+
'Incorporate existing codebase context into your synthesis.',
|
|
436
|
+
'Focus recommendations on what to ADD or CHANGE, not what to build from scratch.'
|
|
437
|
+
].join('\n');
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const prompt = interpolate(synthesisTemplate, {
|
|
441
|
+
research_dir: researchDir,
|
|
442
|
+
output_path: outputPath
|
|
443
|
+
}) + codebaseInstructions;
|
|
444
|
+
|
|
445
|
+
const humanText = [
|
|
446
|
+
'[brain] New project setup - Step 3: Synthesis',
|
|
447
|
+
'',
|
|
448
|
+
'IMPORTANT: Use the Agent tool to spawn a synthesis agent. Do NOT synthesize manually.',
|
|
449
|
+
'Use subagent_type: "brain-synthesizer" with the prompt below.',
|
|
450
|
+
'',
|
|
451
|
+
'After the agent completes, run: npx brain-dev new-project --generate-roadmap',
|
|
452
|
+
hasCodebaseAnalysis ? '[brain] Codebase analysis from .brain/codebase/ will be included in synthesis.' : ''
|
|
453
|
+
].filter(Boolean);
|
|
454
|
+
|
|
455
|
+
output({
|
|
456
|
+
action: 'spawn-synthesis',
|
|
457
|
+
prompt,
|
|
458
|
+
outputPath
|
|
459
|
+
}, humanText.join('\n'));
|
|
460
|
+
|
|
461
|
+
return {
|
|
462
|
+
action: 'spawn-synthesis',
|
|
463
|
+
prompt,
|
|
464
|
+
outputPath
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Step 4: Generate REQUIREMENTS.md with hierarchical IDs and proposed ROADMAP.md.
|
|
470
|
+
*/
|
|
471
|
+
function stepGenerateRoadmap(brainDir) {
|
|
472
|
+
// Read PROJECT.md and research SUMMARY.md
|
|
473
|
+
const projectMdPath = path.join(brainDir, 'PROJECT.md');
|
|
474
|
+
const summaryPath = path.join(brainDir, 'research', 'SUMMARY.md');
|
|
475
|
+
|
|
476
|
+
let projectContent = '';
|
|
477
|
+
let summaryContent = '';
|
|
478
|
+
|
|
479
|
+
if (fs.existsSync(projectMdPath)) {
|
|
480
|
+
projectContent = fs.readFileSync(projectMdPath, 'utf8');
|
|
481
|
+
}
|
|
482
|
+
if (fs.existsSync(summaryPath)) {
|
|
483
|
+
summaryContent = fs.readFileSync(summaryPath, 'utf8');
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Extract features from PROJECT.md
|
|
487
|
+
const features = extractFeatures(projectContent);
|
|
488
|
+
const constraints = extractSection(projectContent, 'Constraints');
|
|
489
|
+
|
|
490
|
+
// Generate REQUIREMENTS.md with hierarchical IDs
|
|
491
|
+
const { requirementsMd, categories } = generateRequirementsMd(features, summaryContent);
|
|
492
|
+
fs.writeFileSync(path.join(brainDir, 'REQUIREMENTS.md'), requirementsMd, 'utf8');
|
|
493
|
+
|
|
494
|
+
// Generate proposed roadmap phases from requirement categories
|
|
495
|
+
const phases = categories.map((cat, i) => ({
|
|
496
|
+
number: i + 1,
|
|
497
|
+
name: cat.name,
|
|
498
|
+
goal: cat.goal,
|
|
499
|
+
dependsOn: i === 0 ? [] : [i],
|
|
500
|
+
requirements: cat.reqIds,
|
|
501
|
+
plans: '',
|
|
502
|
+
status: 'Pending'
|
|
503
|
+
}));
|
|
504
|
+
|
|
505
|
+
// Write proposed ROADMAP.md
|
|
506
|
+
const roadmapData = {
|
|
507
|
+
overview: extractSection(projectContent, 'Vision') || 'Project roadmap',
|
|
508
|
+
phases
|
|
509
|
+
};
|
|
510
|
+
writeRoadmap(brainDir, roadmapData);
|
|
511
|
+
|
|
512
|
+
const humanText = [
|
|
513
|
+
'[brain] New project setup - Step 4: Roadmap proposal',
|
|
514
|
+
'',
|
|
515
|
+
'REQUIREMENTS.md and ROADMAP.md created.',
|
|
516
|
+
'',
|
|
517
|
+
"Here's the proposed roadmap. Present it to the user and ask:",
|
|
518
|
+
'"Modify or confirm this roadmap?"',
|
|
519
|
+
'',
|
|
520
|
+
'After confirmed, run: brain-dev new-project --finalize --project-name "ProjectName"'
|
|
521
|
+
];
|
|
522
|
+
|
|
523
|
+
output({
|
|
524
|
+
action: 'confirm-roadmap',
|
|
525
|
+
phases,
|
|
526
|
+
requirementsPath: path.join(brainDir, 'REQUIREMENTS.md'),
|
|
527
|
+
roadmapPath: path.join(brainDir, 'ROADMAP.md')
|
|
528
|
+
}, humanText.join('\n'));
|
|
529
|
+
|
|
530
|
+
return {
|
|
531
|
+
action: 'confirm-roadmap',
|
|
532
|
+
phases,
|
|
533
|
+
requirementsPath: path.join(brainDir, 'REQUIREMENTS.md'),
|
|
534
|
+
roadmapPath: path.join(brainDir, 'ROADMAP.md')
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Step 5: Finalize - write final artifacts, update brain.json, commit.
|
|
540
|
+
*/
|
|
541
|
+
function stepFinalize(args, brainDir) {
|
|
542
|
+
// Get project name
|
|
543
|
+
const nameIdx = args.indexOf('--project-name');
|
|
544
|
+
const projectName = (nameIdx >= 0 && args[nameIdx + 1]) ? args[nameIdx + 1] : 'Untitled Project';
|
|
545
|
+
|
|
546
|
+
// Read current state
|
|
547
|
+
const state = readState(brainDir);
|
|
548
|
+
if (!state) {
|
|
549
|
+
return { action: 'error', message: 'No brain.json found. Run brain-dev init first.' };
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Read roadmap to get phase info
|
|
553
|
+
let phaseCount = 0;
|
|
554
|
+
let phaseData = [];
|
|
555
|
+
try {
|
|
556
|
+
const roadmap = parseRoadmap(brainDir);
|
|
557
|
+
phaseCount = roadmap.phases.length;
|
|
558
|
+
phaseData = roadmap.phases.map(p => ({
|
|
559
|
+
id: p.number,
|
|
560
|
+
number: p.number,
|
|
561
|
+
name: p.name,
|
|
562
|
+
status: p.status || 'Pending',
|
|
563
|
+
goal: p.goal || ''
|
|
564
|
+
}));
|
|
565
|
+
} catch {
|
|
566
|
+
// Roadmap may not exist yet; that's okay
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Read detection for projectType
|
|
570
|
+
const detection = readDetection(brainDir);
|
|
571
|
+
|
|
572
|
+
// Update state
|
|
573
|
+
state.project.name = projectName;
|
|
574
|
+
state.project.initialized = true;
|
|
575
|
+
state.project.description = extractProjectDescription(brainDir);
|
|
576
|
+
state.project.projectType = detection ? detection.type : 'greenfield';
|
|
577
|
+
state.phase.current = phaseCount > 0 ? 1 : 0;
|
|
578
|
+
state.phase.status = phaseCount > 0 ? 'pending' : 'initialized';
|
|
579
|
+
state.phase.total = phaseCount;
|
|
580
|
+
state.phase.phases = phaseData;
|
|
581
|
+
state.milestone.current = 'v1.0';
|
|
582
|
+
|
|
583
|
+
// Write updated state
|
|
584
|
+
writeState(brainDir, state);
|
|
585
|
+
|
|
586
|
+
const humanText = [
|
|
587
|
+
'[brain] New project setup - Complete!',
|
|
588
|
+
'',
|
|
589
|
+
`Project "${projectName}" initialized with ${phaseCount} phases.`,
|
|
590
|
+
detection ? `Project type: ${detection.type}` : '',
|
|
591
|
+
'',
|
|
592
|
+
'Created:',
|
|
593
|
+
' - PROJECT.md (project vision and context)',
|
|
594
|
+
' - REQUIREMENTS.md (hierarchical requirements)',
|
|
595
|
+
' - ROADMAP.md (development phases)',
|
|
596
|
+
' - brain.json (updated state)',
|
|
597
|
+
'',
|
|
598
|
+
'Run /brain:discuss to refine the first phase, or /brain:plan to start planning.'
|
|
599
|
+
].filter(Boolean);
|
|
600
|
+
|
|
601
|
+
output({
|
|
602
|
+
action: 'finalized',
|
|
603
|
+
projectName,
|
|
604
|
+
phases: phaseCount,
|
|
605
|
+
projectType: detection ? detection.type : 'greenfield',
|
|
606
|
+
files: ['PROJECT.md', 'REQUIREMENTS.md', 'ROADMAP.md', 'brain.json'],
|
|
607
|
+
nextAction: '/brain:discuss'
|
|
608
|
+
}, humanText.join('\n'));
|
|
609
|
+
|
|
610
|
+
return {
|
|
611
|
+
action: 'finalized',
|
|
612
|
+
projectName,
|
|
613
|
+
phases: phaseCount,
|
|
614
|
+
projectType: detection ? detection.type : 'greenfield',
|
|
615
|
+
files: ['PROJECT.md', 'REQUIREMENTS.md', 'ROADMAP.md', 'brain.json'],
|
|
616
|
+
nextAction: '/brain:discuss'
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// ---- Helper functions ----
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Read detection.json from .brain/ directory.
|
|
624
|
+
* @param {string} brainDir
|
|
625
|
+
* @returns {object|null}
|
|
626
|
+
*/
|
|
627
|
+
function readDetection(brainDir) {
|
|
628
|
+
try {
|
|
629
|
+
const detPath = path.join(brainDir, 'detection.json');
|
|
630
|
+
if (!fs.statSync(detPath).isFile()) return null;
|
|
631
|
+
const content = fs.readFileSync(detPath, 'utf8');
|
|
632
|
+
const data = JSON.parse(content);
|
|
633
|
+
if (!data || typeof data !== 'object' || !data.type) return null;
|
|
634
|
+
// Ensure stack.primary exists for safe access
|
|
635
|
+
if (data.stack && !data.stack.primary) {
|
|
636
|
+
data.stack.primary = { language: data.stack.language || null, framework: data.stack.framework || null, runtime: data.stack.runtime || null, dependencies: data.stack.dependencies || [] };
|
|
637
|
+
}
|
|
638
|
+
return data;
|
|
639
|
+
} catch {
|
|
640
|
+
return null;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Build codebase context string for brownfield researcher prompts.
|
|
646
|
+
* @param {object} detection
|
|
647
|
+
* @returns {string}
|
|
648
|
+
*/
|
|
649
|
+
function buildCodebaseContext(detection) {
|
|
650
|
+
const { stack, features, signals, workspace } = detection;
|
|
651
|
+
const primary = stack.primary || stack;
|
|
652
|
+
const lines = [
|
|
653
|
+
'',
|
|
654
|
+
'## Existing Codebase Context',
|
|
655
|
+
'',
|
|
656
|
+
'This is NOT a greenfield project. An existing codebase was detected:',
|
|
657
|
+
];
|
|
658
|
+
|
|
659
|
+
if (primary.framework) {
|
|
660
|
+
lines.push(`- **Primary Stack:** ${primary.framework}${primary.language ? ` (${primary.language})` : ''}`);
|
|
661
|
+
} else if (primary.language) {
|
|
662
|
+
lines.push(`- **Language:** ${primary.language}`);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Frontend stack (when separate from primary, e.g., Laravel + Vue.js)
|
|
666
|
+
if (stack.frontend) {
|
|
667
|
+
const feParts = [stack.frontend.framework, stack.frontend.bundler, stack.frontend.css].filter(Boolean).join(' + ');
|
|
668
|
+
if (feParts) {
|
|
669
|
+
lines.push(`- **Frontend:** ${feParts}`);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
if (features.length > 0) {
|
|
674
|
+
lines.push(`- **Existing features:** ${features.join(', ')}`);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
if (signals.codeFiles > 0) {
|
|
678
|
+
lines.push(`- **Size:** ${signals.codeFiles} source files`);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
if (signals.commitCount > 0) {
|
|
682
|
+
lines.push(`- **History:** ${signals.commitCount} commits`);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const deps = primary.dependencies || [];
|
|
686
|
+
if (deps.length > 0) {
|
|
687
|
+
lines.push(`- **Key dependencies:** ${deps.slice(0, 5).join(', ')}`);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Workspace siblings
|
|
691
|
+
if (workspace && workspace.siblings.length > 0) {
|
|
692
|
+
lines.push('');
|
|
693
|
+
lines.push('### Related Projects in Workspace');
|
|
694
|
+
for (const sib of workspace.siblings) {
|
|
695
|
+
lines.push(`- **${sib.name}:** ${sib.stack.framework || sib.stack.language}`);
|
|
696
|
+
}
|
|
697
|
+
lines.push('');
|
|
698
|
+
lines.push('Consider how this project integrates with sibling projects.');
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
lines.push('');
|
|
702
|
+
lines.push('**IMPORTANT:** Research should focus on EXTENDING/IMPROVING this existing codebase, not starting from scratch.');
|
|
703
|
+
lines.push('Consider compatibility with the existing stack and patterns.');
|
|
704
|
+
lines.push('');
|
|
705
|
+
|
|
706
|
+
return lines.join('\n');
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* Generate PROJECT.md from user answers, marking unknowns as TBD.
|
|
711
|
+
* Optionally includes existing codebase section for brownfield projects.
|
|
712
|
+
* @param {object} answers
|
|
713
|
+
* @param {object} [detection] - Detection result for brownfield projects
|
|
714
|
+
*/
|
|
715
|
+
function generateProjectMd(answers, detection) {
|
|
716
|
+
const tbd = (val) => {
|
|
717
|
+
if (!val) return 'TBD';
|
|
718
|
+
if (Array.isArray(val)) return val.join(', ') || 'TBD';
|
|
719
|
+
if (typeof val !== 'string') return String(val);
|
|
720
|
+
const lower = val.toLowerCase().trim();
|
|
721
|
+
if (lower === "i don't know yet" || lower === 'idk' || lower === 'not sure' || lower === 'tbd') {
|
|
722
|
+
return 'TBD';
|
|
723
|
+
}
|
|
724
|
+
return val;
|
|
725
|
+
};
|
|
726
|
+
|
|
727
|
+
const lines = [
|
|
728
|
+
'# Project',
|
|
729
|
+
'',
|
|
730
|
+
'## Vision',
|
|
731
|
+
tbd(answers.vision),
|
|
732
|
+
'',
|
|
733
|
+
'## Users',
|
|
734
|
+
tbd(answers.users),
|
|
735
|
+
'',
|
|
736
|
+
'## Features',
|
|
737
|
+
tbd(answers.features),
|
|
738
|
+
'',
|
|
739
|
+
'## Constraints',
|
|
740
|
+
tbd(answers.constraints),
|
|
741
|
+
''
|
|
742
|
+
];
|
|
743
|
+
|
|
744
|
+
// Add existing codebase section for brownfield projects
|
|
745
|
+
if (detection && detection.type === 'brownfield') {
|
|
746
|
+
const primary = detection.stack.primary || detection.stack;
|
|
747
|
+
lines.push('## Existing Codebase');
|
|
748
|
+
if (primary.framework) {
|
|
749
|
+
lines.push(`- **Stack:** ${primary.framework}${primary.language ? ` (${primary.language})` : ''}`);
|
|
750
|
+
} else if (primary.language) {
|
|
751
|
+
lines.push(`- **Language:** ${primary.language}`);
|
|
752
|
+
}
|
|
753
|
+
if (detection.stack.frontend) {
|
|
754
|
+
const feParts = [detection.stack.frontend.framework, detection.stack.frontend.bundler, detection.stack.frontend.css].filter(Boolean).join(' + ');
|
|
755
|
+
if (feParts) lines.push(`- **Frontend:** ${feParts}`);
|
|
756
|
+
}
|
|
757
|
+
if (detection.features.length > 0) {
|
|
758
|
+
lines.push(`- **Features:** ${detection.features.join(', ')}`);
|
|
759
|
+
}
|
|
760
|
+
if (detection.signals.codeFiles > 0) {
|
|
761
|
+
lines.push(`- **Size:** ${detection.signals.codeFiles} source files`);
|
|
762
|
+
}
|
|
763
|
+
if (detection.signals.commitCount > 0) {
|
|
764
|
+
lines.push(`- **Commits:** ${detection.signals.commitCount}`);
|
|
765
|
+
}
|
|
766
|
+
lines.push(`- **Status:** Active development`);
|
|
767
|
+
lines.push('');
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
return lines.join('\n');
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* Extract features list from PROJECT.md content.
|
|
775
|
+
*/
|
|
776
|
+
function extractFeatures(content) {
|
|
777
|
+
const featSection = extractSection(content, 'Features');
|
|
778
|
+
if (!featSection) return [];
|
|
779
|
+
|
|
780
|
+
// Split by lines starting with - or numbered items
|
|
781
|
+
return featSection
|
|
782
|
+
.split('\n')
|
|
783
|
+
.map(l => l.replace(/^[-*\d.)\s]+/, '').trim())
|
|
784
|
+
.filter(l => l.length > 0);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* Extract a markdown section's content by heading name.
|
|
789
|
+
*/
|
|
790
|
+
function extractSection(content, heading) {
|
|
791
|
+
const regex = new RegExp(`## ${heading}\\s*\\n([\\s\\S]*?)(?=\\n## |$)`);
|
|
792
|
+
const match = content.match(regex);
|
|
793
|
+
return match ? match[1].trim() : '';
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* Generate REQUIREMENTS.md with hierarchical IDs grouped by category.
|
|
798
|
+
*/
|
|
799
|
+
function generateRequirementsMd(features, summaryContent) {
|
|
800
|
+
// Create categories from features and summary
|
|
801
|
+
const categories = [];
|
|
802
|
+
|
|
803
|
+
// Category 1: Core features (from user's feature list)
|
|
804
|
+
if (features.length > 0) {
|
|
805
|
+
const reqIds = features.map((_, i) => `1.${i + 1}`);
|
|
806
|
+
categories.push({
|
|
807
|
+
name: 'Core Features',
|
|
808
|
+
goal: 'Implement the primary functionality',
|
|
809
|
+
reqIds,
|
|
810
|
+
items: features.map((f, i) => ({ id: `1.${i + 1}`, text: f }))
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// Category 2: Infrastructure (from research summary)
|
|
815
|
+
const infraItems = [];
|
|
816
|
+
if (summaryContent.includes('Stack') || summaryContent.includes('stack')) {
|
|
817
|
+
infraItems.push({ id: '2.1', text: 'Setup recommended technology stack' });
|
|
818
|
+
}
|
|
819
|
+
if (summaryContent.includes('Architecture') || summaryContent.includes('architecture')) {
|
|
820
|
+
infraItems.push({ id: '2.2', text: 'Implement recommended architecture patterns' });
|
|
821
|
+
}
|
|
822
|
+
if (infraItems.length > 0) {
|
|
823
|
+
categories.push({
|
|
824
|
+
name: 'Infrastructure',
|
|
825
|
+
goal: 'Setup project infrastructure and architecture',
|
|
826
|
+
reqIds: infraItems.map(i => i.id),
|
|
827
|
+
items: infraItems
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// Category 3: Quality (testing + security from summary)
|
|
832
|
+
const qualityItems = [];
|
|
833
|
+
if (summaryContent.includes('Testing') || summaryContent.includes('testing')) {
|
|
834
|
+
qualityItems.push({ id: '3.1', text: 'Implement testing strategy' });
|
|
835
|
+
}
|
|
836
|
+
if (summaryContent.includes('Security') || summaryContent.includes('security')) {
|
|
837
|
+
qualityItems.push({ id: '3.2', text: 'Implement security requirements' });
|
|
838
|
+
}
|
|
839
|
+
if (qualityItems.length > 0) {
|
|
840
|
+
categories.push({
|
|
841
|
+
name: 'Quality & Security',
|
|
842
|
+
goal: 'Ensure quality and security standards',
|
|
843
|
+
reqIds: qualityItems.map(i => i.id),
|
|
844
|
+
items: qualityItems
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// Fallback: if no categories created, make a default one
|
|
849
|
+
if (categories.length === 0) {
|
|
850
|
+
categories.push({
|
|
851
|
+
name: 'Setup',
|
|
852
|
+
goal: 'Initialize the project',
|
|
853
|
+
reqIds: ['1.1'],
|
|
854
|
+
items: [{ id: '1.1', text: 'Project initialization' }]
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// Generate markdown
|
|
859
|
+
const lines = ['# Requirements', ''];
|
|
860
|
+
|
|
861
|
+
for (const cat of categories) {
|
|
862
|
+
lines.push(`## ${cat.name}`);
|
|
863
|
+
lines.push('');
|
|
864
|
+
for (const item of cat.items) {
|
|
865
|
+
lines.push(`- **${item.id}** ${item.text}`);
|
|
866
|
+
}
|
|
867
|
+
lines.push('');
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
return { requirementsMd: lines.join('\n'), categories };
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* Extract a short project description from PROJECT.md.
|
|
875
|
+
*/
|
|
876
|
+
function extractProjectDescription(brainDir) {
|
|
877
|
+
try {
|
|
878
|
+
const content = fs.readFileSync(path.join(brainDir, 'PROJECT.md'), 'utf8');
|
|
879
|
+
return extractSection(content, 'Vision') || '';
|
|
880
|
+
} catch {
|
|
881
|
+
return '';
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
module.exports = { run, buildBrownfieldQuestions };
|