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,504 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { readState, writeState, atomicWriteSync } = 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, success } = require('../core.cjs');
|
|
10
|
+
const antiPatterns = require('../anti-patterns.cjs');
|
|
11
|
+
const { buildDebuggerSpawnInstructions } = require('./execute.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}
|
|
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
|
+
* Parse YAML-ish frontmatter from a plan file to extract must_haves.
|
|
30
|
+
* This is a lightweight parser -- not a full YAML parser.
|
|
31
|
+
* @param {string} content - Plan file content
|
|
32
|
+
* @returns {object} { truths: string[], artifacts: object[], key_links: object[] }
|
|
33
|
+
*/
|
|
34
|
+
function extractMustHaves(content) {
|
|
35
|
+
const result = { truths: [], artifacts: [], key_links: [] };
|
|
36
|
+
|
|
37
|
+
// Extract truths
|
|
38
|
+
const truthsMatch = content.match(/truths:\s*\n((?:\s+-\s+"[^"]*"\n?)*)/);
|
|
39
|
+
if (truthsMatch) {
|
|
40
|
+
const lines = truthsMatch[1].match(/"([^"]*)"/g);
|
|
41
|
+
if (lines) {
|
|
42
|
+
result.truths = lines.map(l => l.replace(/"/g, ''));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Extract artifacts
|
|
47
|
+
const artifactsMatch = content.match(/artifacts:\s*\n((?:\s+-\s+(?:path|provides|exports).*\n?)*)/);
|
|
48
|
+
if (artifactsMatch) {
|
|
49
|
+
const paths = artifactsMatch[1].match(/path:\s*"([^"]*)"/g);
|
|
50
|
+
if (paths) {
|
|
51
|
+
result.artifacts = paths.map(p => ({
|
|
52
|
+
path: p.replace(/path:\s*"([^"]*)"/, '$1')
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Extract key_links
|
|
58
|
+
const keyLinksMatch = content.match(/key_links:\s*\n((?:\s+-\s+(?:from|to|via|pattern).*\n?)*)/);
|
|
59
|
+
if (keyLinksMatch) {
|
|
60
|
+
const froms = keyLinksMatch[1].match(/from:\s*"([^"]*)"/g);
|
|
61
|
+
if (froms) {
|
|
62
|
+
result.key_links = froms.map(f => ({
|
|
63
|
+
from: f.replace(/from:\s*"([^"]*)"/, '$1')
|
|
64
|
+
}));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Extract phase-modified file paths from PLAN and SUMMARY frontmatter.
|
|
73
|
+
* @param {string[]} planFiles - Full paths to plan files
|
|
74
|
+
* @param {string[]} summaryFiles - Full paths to summary files
|
|
75
|
+
* @returns {string[]} Deduplicated array of file paths
|
|
76
|
+
*/
|
|
77
|
+
function extractPhaseFiles(planFiles, summaryFiles) {
|
|
78
|
+
const files = new Set();
|
|
79
|
+
|
|
80
|
+
for (const planPath of planFiles) {
|
|
81
|
+
const content = fs.readFileSync(planPath, 'utf8');
|
|
82
|
+
// Extract files_modified from frontmatter
|
|
83
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
84
|
+
if (fmMatch) {
|
|
85
|
+
const fm = fmMatch[1];
|
|
86
|
+
const fmIdx = fm.indexOf('files_modified:');
|
|
87
|
+
if (fmIdx >= 0) {
|
|
88
|
+
const afterFm = fm.slice(fmIdx + 'files_modified:'.length);
|
|
89
|
+
const lines = afterFm.split('\n');
|
|
90
|
+
for (const line of lines) {
|
|
91
|
+
const itemMatch = line.match(/^\s+-\s+(.+)/);
|
|
92
|
+
if (itemMatch) {
|
|
93
|
+
files.add(itemMatch[1].trim());
|
|
94
|
+
} else if (line.trim() && !line.match(/^\s+-/) && !line.match(/^\s*$/)) {
|
|
95
|
+
break; // Hit next YAML key
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for (const summaryPath of summaryFiles) {
|
|
103
|
+
let content;
|
|
104
|
+
try {
|
|
105
|
+
content = fs.readFileSync(summaryPath, 'utf8');
|
|
106
|
+
} catch {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
// Extract key-files section from SUMMARY frontmatter
|
|
110
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
111
|
+
if (fmMatch) {
|
|
112
|
+
const fm = fmMatch[1];
|
|
113
|
+
const kfIdx = fm.indexOf('key-files:');
|
|
114
|
+
if (kfIdx >= 0) {
|
|
115
|
+
const afterKf = fm.slice(kfIdx + 'key-files:'.length);
|
|
116
|
+
const lines = afterKf.split('\n');
|
|
117
|
+
for (const line of lines) {
|
|
118
|
+
const itemMatch = line.match(/^\s+-\s+(.+)/);
|
|
119
|
+
if (itemMatch) {
|
|
120
|
+
files.add(itemMatch[1].trim().replace(/^["']|["']$/g, ''));
|
|
121
|
+
} else if (line.trim() && !line.match(/^\s+-/) && !line.match(/^\s*$/) && !line.match(/^\s+\w+:/)) {
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return [...files];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Format anti-pattern scan results for template interpolation.
|
|
134
|
+
* @param {{ findings: object[], blockers: object[], warnings: object[] }} results
|
|
135
|
+
* @returns {string} Formatted markdown string
|
|
136
|
+
*/
|
|
137
|
+
function formatAntiPatternResults(results) {
|
|
138
|
+
if (results.findings.length === 0) return 'No anti-patterns detected.';
|
|
139
|
+
const lines = ['### Anti-Pattern Scan Results', ''];
|
|
140
|
+
lines.push(`**Blockers:** ${results.blockers.length}`);
|
|
141
|
+
lines.push(`**Warnings:** ${results.warnings.length}`);
|
|
142
|
+
lines.push('');
|
|
143
|
+
for (const f of results.findings) {
|
|
144
|
+
lines.push(`- [${f.severity}] ${f.name} in ${f.file}:${f.line}`);
|
|
145
|
+
}
|
|
146
|
+
return lines.join('\n');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Run the verify command.
|
|
151
|
+
*
|
|
152
|
+
* Default: Generate verifier instructions for current phase.
|
|
153
|
+
* --phase N: Target specific phase.
|
|
154
|
+
* --save-results <json>: Save verification results and update state.
|
|
155
|
+
*
|
|
156
|
+
* @param {string[]} args - CLI arguments
|
|
157
|
+
* @param {object} [opts] - Options (brainDir for testing)
|
|
158
|
+
* @returns {object} Structured result
|
|
159
|
+
*/
|
|
160
|
+
async function run(args = [], opts = {}) {
|
|
161
|
+
const brainDir = opts.brainDir || path.join(process.cwd(), '.brain');
|
|
162
|
+
const state = readState(brainDir);
|
|
163
|
+
|
|
164
|
+
if (!state) {
|
|
165
|
+
error("No brain state found. Run 'brain-dev init' first.");
|
|
166
|
+
return { error: 'no-state' };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Check for --log-recovery mode
|
|
170
|
+
const logRecoveryIdx = args.indexOf('--log-recovery');
|
|
171
|
+
if (logRecoveryIdx >= 0) {
|
|
172
|
+
return handleLogRecovery(args, logRecoveryIdx, brainDir, state);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Check for --save-results mode
|
|
176
|
+
const saveIdx = args.indexOf('--save-results');
|
|
177
|
+
if (saveIdx >= 0) {
|
|
178
|
+
return handleSaveResults(args, saveIdx, brainDir, state);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Parse --auto-recover flag
|
|
182
|
+
const autoRecover = args.includes('--auto-recover');
|
|
183
|
+
|
|
184
|
+
// Determine phase
|
|
185
|
+
const phaseIdx = args.indexOf('--phase');
|
|
186
|
+
const phaseNumber = phaseIdx >= 0
|
|
187
|
+
? parseInt(args[phaseIdx + 1], 10)
|
|
188
|
+
: state.phase.current;
|
|
189
|
+
|
|
190
|
+
// Find phase directory
|
|
191
|
+
const phaseDir = findPhaseDir(brainDir, phaseNumber);
|
|
192
|
+
if (!phaseDir) {
|
|
193
|
+
error(`Phase ${phaseNumber} directory not found.`);
|
|
194
|
+
return { error: 'phase-not-found' };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Scan for PLAN-*.md files and extract must_haves from each
|
|
198
|
+
const files = fs.readdirSync(phaseDir);
|
|
199
|
+
const planFiles = files.filter(f => /^PLAN-\d+\.md$/.test(f)).sort();
|
|
200
|
+
|
|
201
|
+
const combinedMustHaves = { truths: [], artifacts: [], key_links: [] };
|
|
202
|
+
|
|
203
|
+
for (const planFile of planFiles) {
|
|
204
|
+
const content = fs.readFileSync(path.join(phaseDir, planFile), 'utf8');
|
|
205
|
+
const mustHaves = extractMustHaves(content);
|
|
206
|
+
combinedMustHaves.truths.push(...mustHaves.truths);
|
|
207
|
+
combinedMustHaves.artifacts.push(...mustHaves.artifacts);
|
|
208
|
+
combinedMustHaves.key_links.push(...mustHaves.key_links);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Scan for SUMMARY-*.md files for cross-referencing
|
|
212
|
+
const summaryFiles = files.filter(f => /^SUMMARY-\d+\.md$/.test(f)).sort();
|
|
213
|
+
const summaryPaths = summaryFiles.map(f => path.join(phaseDir, f));
|
|
214
|
+
|
|
215
|
+
// Extract phase-modified files for anti-pattern scanning
|
|
216
|
+
const planFullPaths = planFiles.map(f => path.join(phaseDir, f));
|
|
217
|
+
const phaseFiles = extractPhaseFiles(planFullPaths, summaryPaths);
|
|
218
|
+
|
|
219
|
+
// Run anti-pattern scan on phase files
|
|
220
|
+
const projectDir = path.dirname(brainDir);
|
|
221
|
+
const apResults = antiPatterns.scanFiles(projectDir, { files: phaseFiles });
|
|
222
|
+
|
|
223
|
+
// Build Nyquist section based on state config
|
|
224
|
+
const nyquistEnabled = state.workflow?.nyquist_validation ?? false;
|
|
225
|
+
const nyquistInstructions = [
|
|
226
|
+
'For each must_have truth, search test files for a matching test:',
|
|
227
|
+
'- Extract key words from the truth statement (ignore stop words: can, the, a, is, are, and, or, to, in, for, with)',
|
|
228
|
+
'- Search test file describe/it/test blocks for 60%+ keyword overlap',
|
|
229
|
+
'- Report coverage ratio: N truths with matching tests / M total truths'
|
|
230
|
+
].join('\n');
|
|
231
|
+
const nyquistSection = nyquistEnabled ? nyquistInstructions : 'Nyquist validation not enabled.';
|
|
232
|
+
|
|
233
|
+
// Build verification output path
|
|
234
|
+
const outputPath = path.join(phaseDir, 'VERIFICATION.md');
|
|
235
|
+
|
|
236
|
+
// Get verifier agent metadata and resolve model
|
|
237
|
+
const verifierAgent = getAgent('verifier');
|
|
238
|
+
const model = resolveModel('verifier', state);
|
|
239
|
+
|
|
240
|
+
// Log spawn event
|
|
241
|
+
logEvent(brainDir, phaseNumber, {
|
|
242
|
+
type: 'spawn',
|
|
243
|
+
agent: 'verifier',
|
|
244
|
+
truths: combinedMustHaves.truths.length,
|
|
245
|
+
artifacts: combinedMustHaves.artifacts.length,
|
|
246
|
+
key_links: combinedMustHaves.key_links.length
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// Load verifier template and interpolate
|
|
250
|
+
const template = loadTemplate('verifier');
|
|
251
|
+
const mustHavesFormatted = [
|
|
252
|
+
'**Truths:**',
|
|
253
|
+
...combinedMustHaves.truths.map(t => `- ${t}`),
|
|
254
|
+
'',
|
|
255
|
+
'**Artifacts:**',
|
|
256
|
+
...combinedMustHaves.artifacts.map(a => `- ${a.path}`),
|
|
257
|
+
'',
|
|
258
|
+
'**Key Links:**',
|
|
259
|
+
...combinedMustHaves.key_links.map(k => `- ${k.from}`)
|
|
260
|
+
].join('\n');
|
|
261
|
+
|
|
262
|
+
const prompt = interpolate(template, {
|
|
263
|
+
must_haves: mustHavesFormatted,
|
|
264
|
+
output_path: outputPath,
|
|
265
|
+
anti_pattern_results: formatAntiPatternResults(apResults),
|
|
266
|
+
nyquist_section: nyquistSection
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// Read depth config from state (defaults to 'deep' for backward compatibility)
|
|
270
|
+
const depthConfig = state.depth || 'deep';
|
|
271
|
+
const depthLevelCount = depthConfig === 'shallow' ? 1 : depthConfig === 'standard' ? 2 : 3;
|
|
272
|
+
|
|
273
|
+
const depthLevelDescriptions = [
|
|
274
|
+
'- **Level 1 (Exists):** File exists and is non-empty',
|
|
275
|
+
'- **Level 2 (Substantive):** Contains real implementation, no stubs or TODO-only content',
|
|
276
|
+
'- **Level 3 (Wired):** Properly imported and consumed by dependent modules'
|
|
277
|
+
];
|
|
278
|
+
|
|
279
|
+
// Append depth and scoring instructions
|
|
280
|
+
const depthInstruction = [
|
|
281
|
+
'',
|
|
282
|
+
`## Depth Levels (configured: ${depthConfig})`,
|
|
283
|
+
'',
|
|
284
|
+
`Apply ${depthLevelCount} verification depth level${depthLevelCount > 1 ? 's' : ''} to each artifact:`,
|
|
285
|
+
...depthLevelDescriptions.slice(0, depthLevelCount),
|
|
286
|
+
'',
|
|
287
|
+
'## Scoring',
|
|
288
|
+
'',
|
|
289
|
+
'Report results as: must_haves_verified / must_haves_total',
|
|
290
|
+
'Status levels: passed (100%), gaps_found (<100%), human_needed (automated passed, human gate pending)'
|
|
291
|
+
].join('\n');
|
|
292
|
+
|
|
293
|
+
// Append SUMMARY.md cross-reference paths
|
|
294
|
+
const summarySection = summaryPaths.length > 0
|
|
295
|
+
? '\n\n## SUMMARY.md Cross-References\n\n' + summaryPaths.map(s => `- ${s}`).join('\n')
|
|
296
|
+
: '';
|
|
297
|
+
|
|
298
|
+
const fullPrompt = prompt + depthInstruction + summarySection;
|
|
299
|
+
|
|
300
|
+
// Update state to verifying
|
|
301
|
+
state.phase.status = 'verifying';
|
|
302
|
+
writeState(brainDir, state);
|
|
303
|
+
|
|
304
|
+
const result = {
|
|
305
|
+
action: 'verify-phase',
|
|
306
|
+
phase: phaseNumber,
|
|
307
|
+
must_haves: combinedMustHaves,
|
|
308
|
+
output_path: outputPath,
|
|
309
|
+
prompt: fullPrompt,
|
|
310
|
+
model,
|
|
311
|
+
depth_levels: depthLevelCount,
|
|
312
|
+
depth_config: depthConfig,
|
|
313
|
+
scoring: true,
|
|
314
|
+
summary_paths: summaryPaths,
|
|
315
|
+
anti_pattern_scan: apResults
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
// Add recovery instructions when --auto-recover is set
|
|
319
|
+
if (autoRecover) {
|
|
320
|
+
const recoveryInstructions = buildRecoveryInstructions(phaseNumber, brainDir, state);
|
|
321
|
+
result.recovery = {
|
|
322
|
+
enabled: true,
|
|
323
|
+
max_cycles: 3,
|
|
324
|
+
instructions: recoveryInstructions,
|
|
325
|
+
buildDebuggerForGap: (gapSlug, errorCtx, taskCtx) =>
|
|
326
|
+
buildDebuggerSpawnInstructions(gapSlug, errorCtx, taskCtx, '', state)
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
// Log recovery config event
|
|
330
|
+
logEvent(brainDir, phaseNumber, {
|
|
331
|
+
type: 'recovery_config',
|
|
332
|
+
auto_recover: true,
|
|
333
|
+
max_cycles: 3
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const humanLines = [
|
|
338
|
+
`[brain] Verifier instructions generated for Phase ${phaseNumber}`,
|
|
339
|
+
`[brain] Truths: ${combinedMustHaves.truths.length}`,
|
|
340
|
+
`[brain] Artifacts: ${combinedMustHaves.artifacts.length}`,
|
|
341
|
+
`[brain] Model: ${model}`,
|
|
342
|
+
`[brain] Depth: ${depthConfig} (${depthLevelCount} level${depthLevelCount > 1 ? 's' : ''})`,
|
|
343
|
+
`[brain] Output: ${outputPath}`,
|
|
344
|
+
'',
|
|
345
|
+
fullPrompt
|
|
346
|
+
];
|
|
347
|
+
output(result, humanLines.join('\n'));
|
|
348
|
+
|
|
349
|
+
return result;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Handle --save-results: write VERIFICATION.md and update state.
|
|
354
|
+
*/
|
|
355
|
+
function handleSaveResults(args, saveIdx, brainDir, state) {
|
|
356
|
+
const resultsJson = args[saveIdx + 1];
|
|
357
|
+
if (!resultsJson) {
|
|
358
|
+
error('--save-results requires a JSON argument');
|
|
359
|
+
return { error: 'missing-argument' };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
let results;
|
|
363
|
+
try {
|
|
364
|
+
results = JSON.parse(resultsJson);
|
|
365
|
+
} catch {
|
|
366
|
+
error('Invalid JSON for --save-results');
|
|
367
|
+
return { error: 'invalid-json' };
|
|
368
|
+
}
|
|
369
|
+
if (!results || typeof results !== 'object') {
|
|
370
|
+
error('--save-results requires a JSON object');
|
|
371
|
+
return { error: 'invalid-json' };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Determine phase dir
|
|
375
|
+
const phaseNumber = state.phase.current;
|
|
376
|
+
const phaseDir = findPhaseDir(brainDir, phaseNumber);
|
|
377
|
+
|
|
378
|
+
if (phaseDir) {
|
|
379
|
+
// Write VERIFICATION.md
|
|
380
|
+
const lines = [
|
|
381
|
+
'# Verification Results',
|
|
382
|
+
'',
|
|
383
|
+
`Phase: ${phaseNumber}`,
|
|
384
|
+
`Overall: ${results.passed ? 'PASSED' : 'FAILED'}`,
|
|
385
|
+
'',
|
|
386
|
+
'## Checks',
|
|
387
|
+
''
|
|
388
|
+
];
|
|
389
|
+
|
|
390
|
+
if (Array.isArray(results.checks)) {
|
|
391
|
+
for (const check of results.checks) {
|
|
392
|
+
if (!check || typeof check !== 'object') continue;
|
|
393
|
+
lines.push(`- [${check.passed ? 'x' : ' '}] ${check.name || 'unknown'} (${check.level || '?'})`);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
lines.push('');
|
|
398
|
+
atomicWriteSync(path.join(phaseDir, 'VERIFICATION.md'), lines.join('\n'));
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Update state
|
|
402
|
+
state.phase.status = results.passed ? 'verified' : 'verification-failed';
|
|
403
|
+
writeState(brainDir, state);
|
|
404
|
+
|
|
405
|
+
const msg = results.passed
|
|
406
|
+
? 'Verification passed! Run /brain:complete to finalize.'
|
|
407
|
+
: 'Verification failed. Fix gaps and re-verify.';
|
|
408
|
+
|
|
409
|
+
const nextAction = results.passed ? '/brain:complete' : '/brain:execute';
|
|
410
|
+
output({ action: 'results-saved', passed: results.passed, nextAction }, `[brain] ${msg}`);
|
|
411
|
+
return { action: 'results-saved', passed: results.passed, nextAction };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Build recovery protocol instructions for the verifier agent.
|
|
416
|
+
* These instructions tell the orchestrator how to handle gaps with debugger agents.
|
|
417
|
+
*
|
|
418
|
+
* @param {number} phaseNumber
|
|
419
|
+
* @param {string} brainDir
|
|
420
|
+
* @param {object} state
|
|
421
|
+
* @returns {string} Markdown instruction block
|
|
422
|
+
*/
|
|
423
|
+
function buildRecoveryInstructions(phaseNumber, brainDir, state) {
|
|
424
|
+
return [
|
|
425
|
+
'## Recovery Protocol',
|
|
426
|
+
'',
|
|
427
|
+
'When verification finds gaps, enter autonomous recovery mode:',
|
|
428
|
+
'',
|
|
429
|
+
'### Recovery Loop (max 3 cycles)',
|
|
430
|
+
'',
|
|
431
|
+
'For each cycle (1 to 3):',
|
|
432
|
+
'1. Identify all unresolved gaps from verification results',
|
|
433
|
+
'2. For each gap, spawn a debugger agent using `brain-dev execute --spot-check` context:',
|
|
434
|
+
' - Pass the gap description as error context',
|
|
435
|
+
' - Pass the must_have truth/artifact as task context',
|
|
436
|
+
' - The debugger will diagnose and fix the issue',
|
|
437
|
+
'3. After all debuggers complete, re-verify: `brain-dev verify --phase ' + phaseNumber + ' --save-results`',
|
|
438
|
+
'4. Log progress: `brain-dev verify --log-recovery \'{"type":"re_verify","cycle":N,"passed":false,"remaining_gaps":M}\'`',
|
|
439
|
+
'',
|
|
440
|
+
'### Non-Convergence Detection',
|
|
441
|
+
'',
|
|
442
|
+
'Track gap count between cycles. If the gap count is not decreasing between',
|
|
443
|
+
'cycle N and cycle N+1 (gaps stay same or increase), trigger early escalation',
|
|
444
|
+
'after cycle 2 instead of waiting for cycle 3.',
|
|
445
|
+
'',
|
|
446
|
+
'### Escalation',
|
|
447
|
+
'',
|
|
448
|
+
'After 3 failed cycles (or early escalation on non-convergence):',
|
|
449
|
+
'- Output remaining gaps with descriptions',
|
|
450
|
+
'- Suggest: "Run `/brain:plan --gaps` to create targeted fix plans for remaining gaps"',
|
|
451
|
+
'- Log: `brain-dev verify --log-recovery \'{"type":"recovery_escalated","cycles":N,"remaining_gaps":["gap-slug"]}\'`',
|
|
452
|
+
'',
|
|
453
|
+
'### JSONL Logging',
|
|
454
|
+
'',
|
|
455
|
+
'Log each recovery event via CLI:',
|
|
456
|
+
'- `brain-dev verify --log-recovery \'{"type":"recovery_start","cycle":1,"gaps":N}\'`',
|
|
457
|
+
'- `brain-dev verify --log-recovery \'{"type":"debugger_spawn","cycle":1,"gap":"gap-slug"}\'`',
|
|
458
|
+
'- `brain-dev verify --log-recovery \'{"type":"debugger_result","cycle":1,"gap":"gap-slug","status":"resolved"}\'`',
|
|
459
|
+
'- `brain-dev verify --log-recovery \'{"type":"re_verify","cycle":1,"passed":false,"remaining_gaps":N}\'`',
|
|
460
|
+
'- `brain-dev verify --log-recovery \'{"type":"recovery_complete","cycles":N,"final_status":"passed"}\'`'
|
|
461
|
+
].join('\n');
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Handle --log-recovery: log a recovery event to phase JSONL.
|
|
466
|
+
*
|
|
467
|
+
* @param {string[]} args
|
|
468
|
+
* @param {number} logRecoveryIdx
|
|
469
|
+
* @param {string} brainDir
|
|
470
|
+
* @param {object} state
|
|
471
|
+
* @returns {object}
|
|
472
|
+
*/
|
|
473
|
+
function handleLogRecovery(args, logRecoveryIdx, brainDir, state) {
|
|
474
|
+
const eventJson = args[logRecoveryIdx + 1];
|
|
475
|
+
if (!eventJson) {
|
|
476
|
+
error('--log-recovery requires a JSON argument');
|
|
477
|
+
return { error: 'missing-argument' };
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
let event;
|
|
481
|
+
try {
|
|
482
|
+
event = JSON.parse(eventJson);
|
|
483
|
+
} catch {
|
|
484
|
+
error('Invalid JSON for --log-recovery');
|
|
485
|
+
return { error: 'invalid-json' };
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Determine phase
|
|
489
|
+
const phaseIdx = args.indexOf('--phase');
|
|
490
|
+
const phaseNumber = phaseIdx >= 0
|
|
491
|
+
? parseInt(args[phaseIdx + 1], 10)
|
|
492
|
+
: state.phase.current;
|
|
493
|
+
|
|
494
|
+
logEvent(brainDir, phaseNumber, event);
|
|
495
|
+
|
|
496
|
+
if (!event || typeof event !== 'object') {
|
|
497
|
+
error('--log-recovery requires a JSON object, not null/primitive');
|
|
498
|
+
return { error: 'invalid-json' };
|
|
499
|
+
}
|
|
500
|
+
output({ action: 'recovery-logged', event }, `[brain] Recovery event logged: ${event.type || 'unknown'}`);
|
|
501
|
+
return { action: 'recovery-logged', event };
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
module.exports = { run };
|