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,415 @@
|
|
|
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 { getAgent, resolveModel } = require('../agents.cjs');
|
|
8
|
+
const { logEvent } = require('../logger.cjs');
|
|
9
|
+
const { output, error } = require('../core.cjs');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Find a phase directory under .brain/phases/ matching a phase number.
|
|
13
|
+
* @param {string} brainDir
|
|
14
|
+
* @param {number} phaseNumber
|
|
15
|
+
* @returns {string|null}
|
|
16
|
+
*/
|
|
17
|
+
function findPhaseDir(brainDir, phaseNumber) {
|
|
18
|
+
const phasesDir = path.join(brainDir, 'phases');
|
|
19
|
+
if (!fs.existsSync(phasesDir)) return null;
|
|
20
|
+
|
|
21
|
+
const padded = String(phaseNumber).padStart(2, '0');
|
|
22
|
+
const match = fs.readdirSync(phasesDir).find(d => d.startsWith(`${padded}-`));
|
|
23
|
+
return match ? path.join(phasesDir, match) : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Scan a phase directory for PLAN-*.md files and return them sorted.
|
|
28
|
+
* @param {string} phaseDir
|
|
29
|
+
* @returns {Array<{num: number, path: string, hasSummary: boolean}>}
|
|
30
|
+
*/
|
|
31
|
+
function scanPlans(phaseDir) {
|
|
32
|
+
if (!phaseDir || !fs.existsSync(phaseDir)) return [];
|
|
33
|
+
|
|
34
|
+
const files = fs.readdirSync(phaseDir);
|
|
35
|
+
const plans = [];
|
|
36
|
+
|
|
37
|
+
for (const f of files) {
|
|
38
|
+
const match = f.match(/^PLAN-(\d+)\.md$/);
|
|
39
|
+
if (match) {
|
|
40
|
+
const num = parseInt(match[1], 10);
|
|
41
|
+
const padded = String(num).padStart(2, '0');
|
|
42
|
+
const hasSummary = files.includes(`SUMMARY-${padded}.md`);
|
|
43
|
+
plans.push({
|
|
44
|
+
num,
|
|
45
|
+
path: path.join(phaseDir, f),
|
|
46
|
+
hasSummary
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
plans.sort((a, b) => a.num - b.num);
|
|
52
|
+
return plans;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Extract task names from plan content.
|
|
57
|
+
* @param {string} content - Plan file content
|
|
58
|
+
* @returns {string[]}
|
|
59
|
+
*/
|
|
60
|
+
function extractTasks(content) {
|
|
61
|
+
const tasks = [];
|
|
62
|
+
const taskRegex = /<name>(.*?)<\/name>/g;
|
|
63
|
+
let match;
|
|
64
|
+
while ((match = taskRegex.exec(content)) !== null) {
|
|
65
|
+
tasks.push(match[1].trim());
|
|
66
|
+
}
|
|
67
|
+
return tasks;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Spot-check a completed plan by verifying SUMMARY.md content.
|
|
72
|
+
* Checks key files exist and Self-Check marker is present.
|
|
73
|
+
*
|
|
74
|
+
* @param {string} phaseDir - Phase directory path
|
|
75
|
+
* @param {number} planNum - Plan number
|
|
76
|
+
* @param {string} summaryPath - Path to SUMMARY.md
|
|
77
|
+
* @returns {{ passed: boolean, checks: Array<{check: string, file?: string, passed: boolean}> }}
|
|
78
|
+
*/
|
|
79
|
+
function spotCheckPlan(phaseDir, planNum, summaryPath) {
|
|
80
|
+
const checks = [];
|
|
81
|
+
|
|
82
|
+
// Check SUMMARY.md exists
|
|
83
|
+
if (!fs.existsSync(summaryPath)) {
|
|
84
|
+
checks.push({ check: 'summary-exists', file: summaryPath, passed: false });
|
|
85
|
+
return { passed: false, checks };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const content = fs.readFileSync(summaryPath, 'utf8');
|
|
89
|
+
checks.push({ check: 'summary-exists', file: summaryPath, passed: true });
|
|
90
|
+
|
|
91
|
+
// Check summary is non-empty
|
|
92
|
+
const isNonEmpty = content.trim().length > 0;
|
|
93
|
+
checks.push({ check: 'summary-non-empty', file: summaryPath, passed: isNonEmpty });
|
|
94
|
+
|
|
95
|
+
// Parse "Key Files" section and verify each file exists
|
|
96
|
+
const keyFilesMatch = content.match(/## Key Files[\s\S]*?(?=\n## |\n---|\Z)/);
|
|
97
|
+
if (keyFilesMatch) {
|
|
98
|
+
const filePathRegex = /[-*]\s+`?([^\s`]+\.\w+)`?/g;
|
|
99
|
+
let fileMatch;
|
|
100
|
+
while ((fileMatch = filePathRegex.exec(keyFilesMatch[0])) !== null) {
|
|
101
|
+
const filePath = fileMatch[1];
|
|
102
|
+
// Only check paths that look like real file paths (not headers or descriptions)
|
|
103
|
+
if (filePath.includes('/') || filePath.includes('.')) {
|
|
104
|
+
const projectRoot = path.dirname(path.dirname(phaseDir));
|
|
105
|
+
const fullPath = path.isAbsolute(filePath) ? filePath : path.resolve(projectRoot, filePath);
|
|
106
|
+
// Guard against path traversal: resolved path must be within project root
|
|
107
|
+
if (!fullPath.startsWith(projectRoot + path.sep) && fullPath !== projectRoot) {
|
|
108
|
+
checks.push({ check: 'key-file-exists', file: filePath, passed: false });
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const exists = fs.existsSync(fullPath);
|
|
112
|
+
checks.push({ check: 'key-file-exists', file: filePath, passed: exists });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Check for Self-Check: PASSED marker
|
|
118
|
+
const hasSelfCheck = content.includes('Self-Check: PASSED');
|
|
119
|
+
checks.push({ check: 'self-check-marker', passed: hasSelfCheck });
|
|
120
|
+
|
|
121
|
+
const passed = checks.every(c => c.passed);
|
|
122
|
+
return { passed, checks };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Build failure handling instructions for the executor prompt.
|
|
127
|
+
* Describes auto-fix scope, escalation scope, retry behavior, and debugger spawning.
|
|
128
|
+
*
|
|
129
|
+
* @returns {string} Markdown text block
|
|
130
|
+
*/
|
|
131
|
+
function buildFailureInstructions() {
|
|
132
|
+
return [
|
|
133
|
+
'## Failure Handling',
|
|
134
|
+
'',
|
|
135
|
+
'### Auto-fix Scope (retry automatically)',
|
|
136
|
+
'- Test failures in code you wrote',
|
|
137
|
+
'- Import errors and missing module references',
|
|
138
|
+
'- Type mismatches and incorrect function signatures',
|
|
139
|
+
'- Missing files that should have been created',
|
|
140
|
+
'- Lint and formatting issues',
|
|
141
|
+
'',
|
|
142
|
+
'### Escalate Scope (do NOT retry, output EXECUTION FAILED)',
|
|
143
|
+
'- API contract changes that affect other plans',
|
|
144
|
+
'- New dependencies not in the plan',
|
|
145
|
+
'- Database schema changes',
|
|
146
|
+
'- Architectural deviations from plan',
|
|
147
|
+
'- Changes to shared interfaces',
|
|
148
|
+
'',
|
|
149
|
+
'### Retry Behavior',
|
|
150
|
+
'- On failure from auto-fix scope: retry once automatically',
|
|
151
|
+
'- If auto-retry fails: output EXECUTION FAILED with error context',
|
|
152
|
+
'- The orchestrator will then spawn a debugger agent with the error context',
|
|
153
|
+
'',
|
|
154
|
+
'### Debugger Path',
|
|
155
|
+
'- Debug sessions are stored at: `.brain/debug/issue-{task-slug}.md`',
|
|
156
|
+
'- If a debugger agent is spawned, it will receive the error context, task context, and attempted fixes'
|
|
157
|
+
].join('\n');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Build debugger spawn instructions by interpolating the debugger template with runtime values.
|
|
162
|
+
* Called by the orchestrator when auto-retry fails and a debugger agent must be spawned.
|
|
163
|
+
*
|
|
164
|
+
* @param {string} taskSlug - Slug for the failed task
|
|
165
|
+
* @param {string} errorContext - Error message and stack trace
|
|
166
|
+
* @param {string} taskContext - Description of what the task was doing
|
|
167
|
+
* @param {string} attemptedFixes - What was already tried
|
|
168
|
+
* @param {object} [state] - brain.json state for model resolution
|
|
169
|
+
* @returns {{ action: string, prompt: string, model: string, session_path: string }}
|
|
170
|
+
*/
|
|
171
|
+
function buildDebuggerSpawnInstructions(taskSlug, errorContext, taskContext, attemptedFixes, state) {
|
|
172
|
+
const tpl = loadTemplate('debugger');
|
|
173
|
+
const sessionPath = '.brain/debug/issue-' + taskSlug + '.md';
|
|
174
|
+
|
|
175
|
+
const interpolatedPrompt = interpolate(tpl, {
|
|
176
|
+
error_context: errorContext,
|
|
177
|
+
task_context: taskContext,
|
|
178
|
+
attempted_fixes: attemptedFixes,
|
|
179
|
+
debug_session_path: sessionPath
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
action: 'spawn-debugger',
|
|
184
|
+
prompt: interpolatedPrompt,
|
|
185
|
+
model: resolveModel('debugger', state || null),
|
|
186
|
+
session_path: sessionPath
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Build checkpoint protocol instructions for the executor prompt.
|
|
192
|
+
* Describes how to handle non-autonomous plans with checkpoint gates.
|
|
193
|
+
*
|
|
194
|
+
* @returns {string} Markdown text block
|
|
195
|
+
*/
|
|
196
|
+
function buildCheckpointInstructions() {
|
|
197
|
+
return [
|
|
198
|
+
'## Checkpoint Protocol',
|
|
199
|
+
'',
|
|
200
|
+
'For non-autonomous plans, checkpoints pause execution for human input.',
|
|
201
|
+
'',
|
|
202
|
+
'### checkpoint:human-verify',
|
|
203
|
+
'After completing automated work, present what was built for visual/functional verification.',
|
|
204
|
+
'Output a structured checkpoint block:',
|
|
205
|
+
'```',
|
|
206
|
+
'## CHECKPOINT REACHED',
|
|
207
|
+
'Type: human-verify',
|
|
208
|
+
'Progress: {completed}/{total} tasks',
|
|
209
|
+
'What was built: [description]',
|
|
210
|
+
'Verification steps: [URLs, commands, expected behavior]',
|
|
211
|
+
'```',
|
|
212
|
+
'',
|
|
213
|
+
'### checkpoint:decision',
|
|
214
|
+
'When implementation requires a choice between options.',
|
|
215
|
+
'Output a structured checkpoint block with options table.',
|
|
216
|
+
'',
|
|
217
|
+
'### checkpoint:human-action',
|
|
218
|
+
'When a truly unavoidable manual step is needed (email link, 2FA code).',
|
|
219
|
+
'Output what automation was attempted and the single manual step needed.'
|
|
220
|
+
].join('\n');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Run the execute command.
|
|
225
|
+
*
|
|
226
|
+
* Default: Find the next unexecuted plan in current phase and output executor instructions.
|
|
227
|
+
* --plan NN: Target a specific plan.
|
|
228
|
+
* --phase N: Target a specific phase.
|
|
229
|
+
* --spot-check NN: Run spot-check verification for plan NN.
|
|
230
|
+
*
|
|
231
|
+
* @param {string[]} args - CLI arguments
|
|
232
|
+
* @param {object} [opts] - Options (brainDir for testing)
|
|
233
|
+
* @returns {object} Structured result
|
|
234
|
+
*/
|
|
235
|
+
async function run(args = [], opts = {}) {
|
|
236
|
+
const brainDir = opts.brainDir || path.join(process.cwd(), '.brain');
|
|
237
|
+
const state = readState(brainDir);
|
|
238
|
+
|
|
239
|
+
if (!state) {
|
|
240
|
+
error("No brain state found. Run 'brain-dev init' first.");
|
|
241
|
+
return { error: 'no-state' };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Determine phase
|
|
245
|
+
const phaseIdx = args.indexOf('--phase');
|
|
246
|
+
const phaseNumber = phaseIdx >= 0
|
|
247
|
+
? parseInt(args[phaseIdx + 1], 10)
|
|
248
|
+
: state.phase.current;
|
|
249
|
+
|
|
250
|
+
// Handle --spot-check flag
|
|
251
|
+
const spotCheckIdx = args.indexOf('--spot-check');
|
|
252
|
+
if (spotCheckIdx >= 0) {
|
|
253
|
+
const planNum = parseInt(args[spotCheckIdx + 1], 10);
|
|
254
|
+
const phaseDir = findPhaseDir(brainDir, phaseNumber);
|
|
255
|
+
if (!phaseDir) {
|
|
256
|
+
error(`Phase ${phaseNumber} directory not found.`);
|
|
257
|
+
return { error: 'phase-not-found' };
|
|
258
|
+
}
|
|
259
|
+
const padded = String(planNum).padStart(2, '0');
|
|
260
|
+
const summaryPath = path.join(phaseDir, `SUMMARY-${padded}.md`);
|
|
261
|
+
const checkResult = spotCheckPlan(phaseDir, planNum, summaryPath);
|
|
262
|
+
|
|
263
|
+
logEvent(brainDir, phaseNumber, {
|
|
264
|
+
type: 'spot-check',
|
|
265
|
+
plan: padded,
|
|
266
|
+
passed: checkResult.passed,
|
|
267
|
+
checks: checkResult.checks.length
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
const msg = checkResult.passed
|
|
271
|
+
? `Spot-check PASSED for plan ${padded}`
|
|
272
|
+
: `Spot-check FAILED for plan ${padded}`;
|
|
273
|
+
output({ action: 'spot-check', ...checkResult }, `[brain] ${msg}`);
|
|
274
|
+
return { action: 'spot-check', ...checkResult };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Find phase directory
|
|
278
|
+
const phaseDir = findPhaseDir(brainDir, phaseNumber);
|
|
279
|
+
const plans = scanPlans(phaseDir);
|
|
280
|
+
|
|
281
|
+
if (plans.length === 0) {
|
|
282
|
+
error(`No plans found for phase ${phaseNumber}. Run '/brain:plan' first.`);
|
|
283
|
+
return { error: 'no-plans' };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Determine which plan to execute
|
|
287
|
+
const planFlag = args.indexOf('--plan');
|
|
288
|
+
let targetPlan;
|
|
289
|
+
|
|
290
|
+
if (planFlag >= 0) {
|
|
291
|
+
const planNum = parseInt(args[planFlag + 1], 10);
|
|
292
|
+
targetPlan = plans.find(p => p.num === planNum);
|
|
293
|
+
if (!targetPlan) {
|
|
294
|
+
error(`Plan ${planNum} not found in phase ${phaseNumber}.`);
|
|
295
|
+
return { error: 'plan-not-found' };
|
|
296
|
+
}
|
|
297
|
+
} else {
|
|
298
|
+
// Find next unexecuted plan (no SUMMARY)
|
|
299
|
+
targetPlan = plans.find(p => !p.hasSummary);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// All plans executed
|
|
303
|
+
if (!targetPlan) {
|
|
304
|
+
state.phase.status = 'executed';
|
|
305
|
+
writeState(brainDir, state);
|
|
306
|
+
const msg = "All plans executed. Run /brain:verify to check the work.";
|
|
307
|
+
output({ action: 'all-executed', message: msg, nextAction: '/brain:verify' }, `[brain] ${msg}`);
|
|
308
|
+
return { action: 'all-executed', message: msg, nextAction: '/brain:verify' };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Read plan content
|
|
312
|
+
const planContent = fs.readFileSync(targetPlan.path, 'utf8');
|
|
313
|
+
const tasks = extractTasks(planContent);
|
|
314
|
+
|
|
315
|
+
// Build summary path
|
|
316
|
+
const padded = String(targetPlan.num).padStart(2, '0');
|
|
317
|
+
const summaryPath = path.join(phaseDir, `SUMMARY-${padded}.md`);
|
|
318
|
+
|
|
319
|
+
// Extract phase/plan info from plan frontmatter
|
|
320
|
+
const phaseMatch = planContent.match(/^phase:\s*(.+)$/m);
|
|
321
|
+
const planNumMatch = planContent.match(/^plan:\s*(.+)$/m);
|
|
322
|
+
|
|
323
|
+
// Check autonomous flag from frontmatter
|
|
324
|
+
const autonomousMatch = planContent.match(/^autonomous:\s*(true|false)/m);
|
|
325
|
+
const autonomous = autonomousMatch ? autonomousMatch[1] === 'true' : true;
|
|
326
|
+
|
|
327
|
+
// Get executor agent metadata and resolve model
|
|
328
|
+
const executorAgent = getAgent('executor');
|
|
329
|
+
const model = resolveModel('executor', state);
|
|
330
|
+
|
|
331
|
+
// Log spawn event
|
|
332
|
+
logEvent(brainDir, phaseNumber, {
|
|
333
|
+
type: 'spawn',
|
|
334
|
+
agent: 'executor',
|
|
335
|
+
plan: padded,
|
|
336
|
+
autonomous,
|
|
337
|
+
model
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// Build failure and checkpoint instructions
|
|
341
|
+
const failureInstructions = buildFailureInstructions();
|
|
342
|
+
const checkpointInstructions = autonomous ? '' : buildCheckpointInstructions();
|
|
343
|
+
|
|
344
|
+
const spotCheckInstruction = 'After completing all tasks, the orchestrator will verify SUMMARY.md, file existence, and commit count.';
|
|
345
|
+
const debuggerSpawnInstruction = 'If auto-retry fails, a debugger agent will be spawned with the error context.';
|
|
346
|
+
|
|
347
|
+
// Load executor template and interpolate
|
|
348
|
+
const template = loadTemplate('executor');
|
|
349
|
+
const prompt = interpolate(template, {
|
|
350
|
+
plan_path: targetPlan.path,
|
|
351
|
+
summary_path: summaryPath,
|
|
352
|
+
plan_content: planContent,
|
|
353
|
+
phase: phaseMatch ? phaseMatch[1].trim() : String(phaseNumber),
|
|
354
|
+
plan_number: planNumMatch ? planNumMatch[1].trim() : String(targetPlan.num),
|
|
355
|
+
subsystem: phaseMatch ? phaseMatch[1].trim() : String(phaseNumber)
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// Append orchestration instructions to prompt
|
|
359
|
+
const fullPrompt = [
|
|
360
|
+
prompt,
|
|
361
|
+
'',
|
|
362
|
+
failureInstructions,
|
|
363
|
+
checkpointInstructions ? '\n' + checkpointInstructions : '',
|
|
364
|
+
'',
|
|
365
|
+
`> ${spotCheckInstruction}`,
|
|
366
|
+
`> ${debuggerSpawnInstruction}`
|
|
367
|
+
].join('\n');
|
|
368
|
+
|
|
369
|
+
// Update state to executing
|
|
370
|
+
state.phase.status = 'executing';
|
|
371
|
+
writeState(brainDir, state);
|
|
372
|
+
|
|
373
|
+
// Check auto-recover config
|
|
374
|
+
const autoRecover = state.workflow?.auto_recover === true;
|
|
375
|
+
|
|
376
|
+
const result = {
|
|
377
|
+
action: 'execute-plan',
|
|
378
|
+
plan_path: targetPlan.path,
|
|
379
|
+
summary_path: summaryPath,
|
|
380
|
+
tasks,
|
|
381
|
+
prompt: fullPrompt,
|
|
382
|
+
model,
|
|
383
|
+
autonomous,
|
|
384
|
+
spot_check: true,
|
|
385
|
+
failure_handling: 'retry-then-debugger',
|
|
386
|
+
auto_recover: autoRecover,
|
|
387
|
+
debugger_spawn: (taskSlug, errorCtx, taskCtx, attemptedFixes) =>
|
|
388
|
+
buildDebuggerSpawnInstructions(taskSlug, errorCtx, taskCtx, attemptedFixes, state)
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
// Build verify command with optional --auto-recover
|
|
392
|
+
const verifyCmd = autoRecover
|
|
393
|
+
? `brain-dev verify --phase ${phaseNumber} --auto-recover`
|
|
394
|
+
: `brain-dev verify --phase ${phaseNumber}`;
|
|
395
|
+
|
|
396
|
+
const humanLines = [
|
|
397
|
+
`[brain] Executor instructions generated for Plan ${padded} in Phase ${phaseNumber}`,
|
|
398
|
+
`[brain] Plan: ${targetPlan.path}`,
|
|
399
|
+
`[brain] Summary output: ${summaryPath}`,
|
|
400
|
+
`[brain] Tasks: ${tasks.length}`,
|
|
401
|
+
`[brain] Model: ${model}`,
|
|
402
|
+
`[brain] Autonomous: ${autonomous}`,
|
|
403
|
+
];
|
|
404
|
+
if (autoRecover) {
|
|
405
|
+
humanLines.push('[brain] Auto-recovery: enabled (verify will auto-diagnose failures)');
|
|
406
|
+
}
|
|
407
|
+
humanLines.push(`[brain] Verify with: ${verifyCmd}`);
|
|
408
|
+
humanLines.push('');
|
|
409
|
+
humanLines.push(fullPrompt);
|
|
410
|
+
output(result, humanLines.join('\n'));
|
|
411
|
+
|
|
412
|
+
return result;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
module.exports = { run, spotCheckPlan, buildDebuggerSpawnInstructions, buildFailureInstructions, buildCheckpointInstructions };
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const { runChecks, autoRepair, generateReport, quickCheck, FIX_MODE_REPAIRS } = require('../health.cjs');
|
|
5
|
+
const { output, error, success } = require('../core.cjs');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Health command handler.
|
|
9
|
+
* Runs health diagnostics and auto-repairs safe issues.
|
|
10
|
+
*
|
|
11
|
+
* Flags:
|
|
12
|
+
* --fix Enable aggressive repair (FIX_MODE_REPAIRS)
|
|
13
|
+
* --quick Run quickCheck only (for bootstrap)
|
|
14
|
+
* --json Force JSON output
|
|
15
|
+
*
|
|
16
|
+
* @param {string[]} args - CLI arguments
|
|
17
|
+
* @param {object} [opts] - Options (brainDir override)
|
|
18
|
+
* @returns {object} Result object
|
|
19
|
+
*/
|
|
20
|
+
async function run(args = [], opts = {}) {
|
|
21
|
+
const brainDir = opts.brainDir || path.join(process.cwd(), '.brain');
|
|
22
|
+
const fix = args.includes('--fix');
|
|
23
|
+
const quick = args.includes('--quick');
|
|
24
|
+
const json = args.includes('--json');
|
|
25
|
+
|
|
26
|
+
// Quick mode: run safe checks only, output pass/fail
|
|
27
|
+
if (quick) {
|
|
28
|
+
const ok = quickCheck(brainDir);
|
|
29
|
+
const result = { action: 'health-quick', passed: ok };
|
|
30
|
+
if (json) {
|
|
31
|
+
console.log(JSON.stringify(result));
|
|
32
|
+
} else {
|
|
33
|
+
if (ok) {
|
|
34
|
+
success('Health: all safe checks passed');
|
|
35
|
+
} else {
|
|
36
|
+
error('Health: some safe checks failed (auto-repaired)');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Full mode
|
|
43
|
+
const results = runChecks(brainDir);
|
|
44
|
+
|
|
45
|
+
// Auto-repair safe-category failures
|
|
46
|
+
const repaired = autoRepair(brainDir, results);
|
|
47
|
+
|
|
48
|
+
// Aggressive fix mode: run FIX_MODE_REPAIRS for fixable report-category failures
|
|
49
|
+
const fixRepaired = [];
|
|
50
|
+
if (fix) {
|
|
51
|
+
for (const r of results) {
|
|
52
|
+
if (r.status === 'fail' && r.category === 'report' && FIX_MODE_REPAIRS[r.name]) {
|
|
53
|
+
try {
|
|
54
|
+
FIX_MODE_REPAIRS[r.name](brainDir);
|
|
55
|
+
fixRepaired.push(r.name);
|
|
56
|
+
r.status = 'repaired';
|
|
57
|
+
} catch (e) {
|
|
58
|
+
r.fixError = e.message;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const report = generateReport(results);
|
|
65
|
+
const allPassed = report.failed === 0;
|
|
66
|
+
|
|
67
|
+
const resultObj = {
|
|
68
|
+
action: 'health-check',
|
|
69
|
+
passed: allPassed,
|
|
70
|
+
results: report,
|
|
71
|
+
repaired: [...repaired, ...fixRepaired]
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
if (json) {
|
|
75
|
+
console.log(JSON.stringify(resultObj, null, 2));
|
|
76
|
+
} else {
|
|
77
|
+
// Human-readable table
|
|
78
|
+
const lines = ['', '[brain] Health Check Results', ''];
|
|
79
|
+
const nameWidth = Math.max(...report.checks.map(c => c.name.length), 4);
|
|
80
|
+
lines.push(` ${'Check'.padEnd(nameWidth)} Status Category Message`);
|
|
81
|
+
lines.push(` ${''.padEnd(nameWidth, '-')} -------- -------- -------`);
|
|
82
|
+
for (const c of report.checks) {
|
|
83
|
+
const statusIcon = c.status === 'pass' ? 'PASS' :
|
|
84
|
+
c.status === 'repaired' ? 'FIXED' : 'FAIL';
|
|
85
|
+
lines.push(` ${c.name.padEnd(nameWidth)} ${statusIcon.padEnd(8)} ${c.category.padEnd(8)} ${c.message}`);
|
|
86
|
+
}
|
|
87
|
+
lines.push('');
|
|
88
|
+
lines.push(` Total: ${report.total} Passed: ${report.passed} Failed: ${report.failed}`);
|
|
89
|
+
if (repaired.length > 0) {
|
|
90
|
+
lines.push(` Auto-repaired: ${repaired.join(', ')}`);
|
|
91
|
+
}
|
|
92
|
+
if (fixRepaired.length > 0) {
|
|
93
|
+
lines.push(` Fix-repaired: ${fixRepaired.join(', ')}`);
|
|
94
|
+
}
|
|
95
|
+
lines.push('');
|
|
96
|
+
|
|
97
|
+
output(resultObj, lines.join('\n'));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return resultObj;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
module.exports = { run };
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { getAgent, resolveModel } = require('../agents.cjs');
|
|
6
|
+
const { loadTemplate, interpolate } = require('../templates.cjs');
|
|
7
|
+
const { readState } = require('../state.cjs');
|
|
8
|
+
const { scanFiles } = require('../security.cjs');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Focus area to output file mapping.
|
|
12
|
+
* Each mapper focus produces specific structured documents.
|
|
13
|
+
*/
|
|
14
|
+
const FOCUS_OUTPUT_MAP = {
|
|
15
|
+
tech: ['STACK.md', 'INTEGRATIONS.md'],
|
|
16
|
+
arch: ['ARCHITECTURE.md', 'STRUCTURE.md'],
|
|
17
|
+
quality: ['CONVENTIONS.md', 'TESTING.md'],
|
|
18
|
+
concerns: ['CONCERNS.md']
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Run the /brain:map command.
|
|
23
|
+
* Generates mapper agent spawn instructions for codebase analysis.
|
|
24
|
+
*
|
|
25
|
+
* @param {string[]} args - CLI arguments
|
|
26
|
+
* @param {object} [opts] - Options
|
|
27
|
+
* @param {string} [opts.brainDir] - Path to .brain/ directory
|
|
28
|
+
* @returns {object} Spawn instructions or error
|
|
29
|
+
*/
|
|
30
|
+
function run(args = [], opts = {}) {
|
|
31
|
+
const brainDir = opts.brainDir || path.join(process.cwd(), '.brain');
|
|
32
|
+
const state = readState(brainDir);
|
|
33
|
+
|
|
34
|
+
if (!state) {
|
|
35
|
+
return { error: 'no-state' };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const mapper = getAgent('mapper');
|
|
39
|
+
const codebaseRoot = process.cwd();
|
|
40
|
+
const outputDir = path.join(codebaseRoot, '.brain', 'codebase');
|
|
41
|
+
|
|
42
|
+
// Parse --focus flag
|
|
43
|
+
let focusAreas = mapper.focus;
|
|
44
|
+
const focusIdx = args.indexOf('--focus');
|
|
45
|
+
if (focusIdx !== -1 && args[focusIdx + 1]) {
|
|
46
|
+
const requested = args[focusIdx + 1];
|
|
47
|
+
if (!FOCUS_OUTPUT_MAP[requested]) {
|
|
48
|
+
return { error: 'invalid-focus', message: `Unknown focus: ${requested}. Valid: ${Object.keys(FOCUS_OUTPUT_MAP).join(', ')}` };
|
|
49
|
+
}
|
|
50
|
+
focusAreas = [requested];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Load mapper template
|
|
54
|
+
let template;
|
|
55
|
+
try {
|
|
56
|
+
template = loadTemplate(mapper.template);
|
|
57
|
+
} catch {
|
|
58
|
+
return { error: 'template-missing', message: `Could not load mapper template` };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Build spawn instructions for each focus area
|
|
62
|
+
const focusAgents = focusAreas.map(focus => {
|
|
63
|
+
const prompt = interpolate(template, {
|
|
64
|
+
focus,
|
|
65
|
+
codebase_root: codebaseRoot,
|
|
66
|
+
output_dir: outputDir
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const model = resolveModel('mapper', state);
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
focus,
|
|
73
|
+
prompt,
|
|
74
|
+
model,
|
|
75
|
+
output_files: FOCUS_OUTPUT_MAP[focus]
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Read parallelization setting
|
|
80
|
+
const parallel = state.workflow?.mapper_parallelization ?? true;
|
|
81
|
+
|
|
82
|
+
// Run security scan on output directory
|
|
83
|
+
let scan_results = null;
|
|
84
|
+
try {
|
|
85
|
+
if (fs.existsSync(outputDir)) {
|
|
86
|
+
scan_results = scanFiles(outputDir);
|
|
87
|
+
}
|
|
88
|
+
} catch {
|
|
89
|
+
// Scan failure is non-fatal; leave scan_results as null
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
action: 'spawn-mapper',
|
|
94
|
+
focus_agents: focusAgents,
|
|
95
|
+
parallel,
|
|
96
|
+
scan_results,
|
|
97
|
+
security_note: 'Security scan runs on .brain/codebase/ output directory. On first run scan_results will be null since mapper agents have not produced output yet.'
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = { run };
|