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