brain-dev 1.2.6 → 2.0.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/bin/lib/commands/auto.cjs +13 -4
- package/bin/lib/commands/discuss.cjs +3 -1
- package/bin/lib/commands/new-project.cjs +39 -0
- package/bin/lib/commands/new-task.cjs +14 -2
- package/bin/lib/commands/pause.cjs +124 -4
- package/bin/lib/commands/plan.cjs +102 -2
- package/bin/lib/commands/progress.cjs +17 -1
- package/bin/lib/commands/resume.cjs +50 -1
- package/bin/lib/commands/story.cjs +28 -4
- package/bin/lib/commands/update.cjs +4 -1
- package/bin/lib/commands.cjs +20 -1
- package/bin/lib/recovery.cjs +2 -3
- package/bin/lib/state.cjs +13 -1
- package/bin/lib/stuck.cjs +71 -0
- package/bin/templates/discuss.md +5 -0
- package/bin/templates/executor.md +37 -3
- package/bin/templates/planner.md +62 -0
- package/bin/templates/researcher.md +3 -0
- package/bin/templates/verifier.md +36 -4
- package/package.json +1 -1
|
@@ -242,10 +242,19 @@ function buildInstructions(runbook, brainDir) {
|
|
|
242
242
|
lines.push('');
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
-
lines.push('### Error
|
|
246
|
-
lines.push('
|
|
247
|
-
lines.push('
|
|
248
|
-
lines.push('
|
|
245
|
+
lines.push('### Error Recovery Decision Tree');
|
|
246
|
+
lines.push('');
|
|
247
|
+
lines.push('If a step fails, use this decision tree:');
|
|
248
|
+
lines.push('');
|
|
249
|
+
lines.push('1. **Test failure**: Re-run `npx brain-dev execute --plan <failed-plan-id>` targeting the specific plan');
|
|
250
|
+
lines.push('2. **Stuck/timeout**: Run `npx brain-dev recover --fix` to clear lock and resume');
|
|
251
|
+
lines.push('3. **Budget exceeded**: Run `npx brain-dev auto --stop` and report to user');
|
|
252
|
+
lines.push('4. **Verification gaps_found**: Re-run execute for the gap files, then re-verify');
|
|
253
|
+
lines.push('5. **Same step fails 2 times**: Stop auto mode — `npx brain-dev auto --stop`');
|
|
254
|
+
lines.push('');
|
|
255
|
+
lines.push('**Loop guard**: If the same step is attempted 3 times, stop auto mode with LOOP DETECTED status.');
|
|
256
|
+
lines.push('');
|
|
257
|
+
lines.push('Between each step, check: `npx brain-dev progress --cost` for budget status.');
|
|
249
258
|
lines.push('');
|
|
250
259
|
lines.push('### Completion');
|
|
251
260
|
lines.push('- When all phases complete or auto stops, run: `npx brain-dev auto --stop` to release the lock');
|
|
@@ -6,6 +6,7 @@ const { readState, writeState, atomicWriteSync } = require('../state.cjs');
|
|
|
6
6
|
const { parseRoadmap } = require('../roadmap.cjs');
|
|
7
7
|
const { loadTemplate, interpolate } = require('../templates.cjs');
|
|
8
8
|
const { output, error, success } = require('../core.cjs');
|
|
9
|
+
const { generateExpertise } = require('../stack-expert.cjs');
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Find the phase directory under .brain/phases/ matching a phase number.
|
|
@@ -126,7 +127,8 @@ function handleAnalyze(args, brainDir, state) {
|
|
|
126
127
|
phase_name: phase.name,
|
|
127
128
|
phase_goal: phase.goal,
|
|
128
129
|
phase_requirements: (Array.isArray(phase.requirements) ? phase.requirements.join(', ') : '') || 'None specified',
|
|
129
|
-
research_section: researchSection
|
|
130
|
+
research_section: researchSection,
|
|
131
|
+
stack_expertise: generateExpertise(brainDir, 'planner')
|
|
130
132
|
});
|
|
131
133
|
|
|
132
134
|
// Update status to discussing
|
|
@@ -7,6 +7,41 @@ const { detectProject } = require('../detect.cjs');
|
|
|
7
7
|
const { output, error } = require('../core.cjs');
|
|
8
8
|
const { readDetection } = require('../story-helpers.cjs');
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Build detailed detection breakdown lines for human display.
|
|
12
|
+
* @param {object} detection
|
|
13
|
+
* @returns {string[]}
|
|
14
|
+
*/
|
|
15
|
+
function buildDetectionDetails(detection) {
|
|
16
|
+
if (!detection || detection.type === 'greenfield') return [];
|
|
17
|
+
|
|
18
|
+
const lines = [];
|
|
19
|
+
const stack = detection.stack || {};
|
|
20
|
+
const primary = stack.primary || {};
|
|
21
|
+
const frontend = stack.frontend || {};
|
|
22
|
+
|
|
23
|
+
if (primary.language || primary.framework) {
|
|
24
|
+
const parts = [primary.language, primary.framework, primary.runtime].filter(Boolean);
|
|
25
|
+
lines.push(`[brain] Stack: ${parts.join(' + ')}`);
|
|
26
|
+
}
|
|
27
|
+
if (frontend.framework) {
|
|
28
|
+
lines.push(`[brain] Frontend: ${frontend.framework}${frontend.bundler ? ' (' + frontend.bundler + ')' : ''}`);
|
|
29
|
+
}
|
|
30
|
+
if (detection.features && detection.features.length > 0) {
|
|
31
|
+
lines.push(`[brain] Features: ${detection.features.slice(0, 6).join(', ')}`);
|
|
32
|
+
}
|
|
33
|
+
if (detection.signals) {
|
|
34
|
+
if (detection.signals.codeFiles) {
|
|
35
|
+
lines.push(`[brain] Source files: ${detection.signals.codeFiles}`);
|
|
36
|
+
}
|
|
37
|
+
if (detection.signals.commitCount) {
|
|
38
|
+
lines.push(`[brain] Git commits: ${detection.signals.commitCount}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return lines;
|
|
43
|
+
}
|
|
44
|
+
|
|
10
45
|
/**
|
|
11
46
|
* Run the new-project command.
|
|
12
47
|
* Simplified 2-step flow:
|
|
@@ -82,8 +117,12 @@ function stepQuestions(brainDir, rootDir) {
|
|
|
82
117
|
? `Brain detected your existing ${detection.summary}.`
|
|
83
118
|
: 'No existing code detected — starting fresh.';
|
|
84
119
|
|
|
120
|
+
// Build detailed detection breakdown
|
|
121
|
+
const detailLines = buildDetectionDetails(detection);
|
|
122
|
+
|
|
85
123
|
const humanText = [
|
|
86
124
|
`[brain] ${detectionSummary}`,
|
|
125
|
+
...detailLines,
|
|
87
126
|
'',
|
|
88
127
|
'IMPORTANT: Use the AskUserQuestion tool NOW to ask the user what they want to do.',
|
|
89
128
|
'Do NOT print the options as text. Use AskUserQuestion with these exact parameters:',
|
|
@@ -263,7 +263,13 @@ function handleContinue(brainDir, state) {
|
|
|
263
263
|
}
|
|
264
264
|
|
|
265
265
|
const taskDir = path.join(tasksDir, dirs[0]);
|
|
266
|
-
|
|
266
|
+
let taskMeta;
|
|
267
|
+
try {
|
|
268
|
+
taskMeta = JSON.parse(fs.readFileSync(path.join(taskDir, 'task.json'), 'utf8'));
|
|
269
|
+
} catch {
|
|
270
|
+
error('Corrupt task.json. Delete the task directory and recreate.');
|
|
271
|
+
return { error: 'corrupt-task-meta' };
|
|
272
|
+
}
|
|
267
273
|
|
|
268
274
|
// Determine current step based on what files exist
|
|
269
275
|
const hasContext = fs.existsSync(path.join(taskDir, 'CONTEXT.md'));
|
|
@@ -406,7 +412,13 @@ function handlePromote(brainDir, state, taskNum) {
|
|
|
406
412
|
}
|
|
407
413
|
|
|
408
414
|
const taskDir = path.join(tasksDir, dirs[0]);
|
|
409
|
-
|
|
415
|
+
let taskMeta;
|
|
416
|
+
try {
|
|
417
|
+
taskMeta = JSON.parse(fs.readFileSync(path.join(taskDir, 'task.json'), 'utf8'));
|
|
418
|
+
} catch {
|
|
419
|
+
error('Corrupt task.json. Cannot promote.');
|
|
420
|
+
return { error: 'corrupt-task-meta' };
|
|
421
|
+
}
|
|
410
422
|
|
|
411
423
|
// Insert as a new phase after current phase
|
|
412
424
|
try {
|
|
@@ -5,6 +5,95 @@ const path = require('node:path');
|
|
|
5
5
|
const { readState, writeState, atomicWriteSync } = require('../state.cjs');
|
|
6
6
|
const { output, prefix, success, error } = require('../core.cjs');
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Read CONTEXT.md from current phase directory.
|
|
10
|
+
* Extracts locked decisions and specific approaches sections.
|
|
11
|
+
* @param {object} state
|
|
12
|
+
* @returns {string|null}
|
|
13
|
+
*/
|
|
14
|
+
function readPhaseContext(state) {
|
|
15
|
+
const phase = state.phase || {};
|
|
16
|
+
const phaseNumber = phase.current || 0;
|
|
17
|
+
if (!phaseNumber) return null;
|
|
18
|
+
|
|
19
|
+
const padded = String(phaseNumber).padStart(2, '0');
|
|
20
|
+
const phasesDir = path.join(process.cwd(), '.brain', 'phases');
|
|
21
|
+
if (!fs.existsSync(phasesDir)) return null;
|
|
22
|
+
|
|
23
|
+
const dirs = fs.readdirSync(phasesDir).filter(d => d.startsWith(padded + '-'));
|
|
24
|
+
if (dirs.length === 0) return null;
|
|
25
|
+
|
|
26
|
+
const contextPath = path.join(phasesDir, dirs[0], 'CONTEXT.md');
|
|
27
|
+
if (!fs.existsSync(contextPath)) return null;
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const content = fs.readFileSync(contextPath, 'utf8');
|
|
31
|
+
// Extract key sections (decisions, approaches) — truncate to keep snapshot manageable
|
|
32
|
+
const lines = content.split('\n');
|
|
33
|
+
const extracted = [];
|
|
34
|
+
let inSection = false;
|
|
35
|
+
for (const line of lines) {
|
|
36
|
+
if (line.match(/^##?\s+(decision|locked|approach|specific)/i)) {
|
|
37
|
+
inSection = true;
|
|
38
|
+
} else if (line.match(/^##?\s+/) && inSection) {
|
|
39
|
+
inSection = false;
|
|
40
|
+
}
|
|
41
|
+
if (inSection) extracted.push(line);
|
|
42
|
+
}
|
|
43
|
+
return extracted.length > 0 ? extracted.join('\n').trim() : content.slice(0, 1500);
|
|
44
|
+
} catch { return null; }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Read current plan's must_haves and progress from the active phase.
|
|
49
|
+
* @param {object} state
|
|
50
|
+
* @returns {string|null}
|
|
51
|
+
*/
|
|
52
|
+
function readCurrentPlanState(state) {
|
|
53
|
+
const phase = state.phase || {};
|
|
54
|
+
const phaseNumber = phase.current || 0;
|
|
55
|
+
if (!phaseNumber) return null;
|
|
56
|
+
|
|
57
|
+
const padded = String(phaseNumber).padStart(2, '0');
|
|
58
|
+
const phasesDir = path.join(process.cwd(), '.brain', 'phases');
|
|
59
|
+
if (!fs.existsSync(phasesDir)) return null;
|
|
60
|
+
|
|
61
|
+
const dirs = fs.readdirSync(phasesDir).filter(d => d.startsWith(padded + '-'));
|
|
62
|
+
if (dirs.length === 0) return null;
|
|
63
|
+
|
|
64
|
+
const phaseDir = path.join(phasesDir, dirs[0]);
|
|
65
|
+
try {
|
|
66
|
+
const files = fs.readdirSync(phaseDir);
|
|
67
|
+
const planFiles = files.filter(f => f.startsWith('PLAN')).sort();
|
|
68
|
+
const summaryFiles = files.filter(f => f.toUpperCase().startsWith('SUMMARY')).sort();
|
|
69
|
+
|
|
70
|
+
const lines = [];
|
|
71
|
+
lines.push(`- Phase: ${phaseNumber} (${phase.status || 'unknown'})`);
|
|
72
|
+
lines.push(`- Plans: ${planFiles.length} total, ${summaryFiles.length} completed`);
|
|
73
|
+
|
|
74
|
+
// Extract must_haves from the latest incomplete plan
|
|
75
|
+
for (const planFile of planFiles) {
|
|
76
|
+
const planNum = planFile.match(/PLAN-(\d+)/)?.[1];
|
|
77
|
+
const hasSummary = summaryFiles.some(s => s.includes(planNum));
|
|
78
|
+
if (!hasSummary && planNum) {
|
|
79
|
+
const planContent = fs.readFileSync(path.join(phaseDir, planFile), 'utf8');
|
|
80
|
+
// Extract truths from must_haves
|
|
81
|
+
const truthMatch = planContent.match(/truths:\s*\n((?:\s+-\s+.+\n?)+)/);
|
|
82
|
+
if (truthMatch) {
|
|
83
|
+
lines.push(`- Active plan: ${planFile}`);
|
|
84
|
+
lines.push('- Must-haves:');
|
|
85
|
+
for (const truth of truthMatch[1].split('\n').filter(l => l.trim().startsWith('-'))) {
|
|
86
|
+
lines.push(` ${truth.trim()}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
break; // Only show first incomplete plan
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return lines.join('\n');
|
|
94
|
+
} catch { return null; }
|
|
95
|
+
}
|
|
96
|
+
|
|
8
97
|
/**
|
|
9
98
|
* Parse --note flag from args.
|
|
10
99
|
* @param {string[]} args
|
|
@@ -72,11 +161,44 @@ function generateSnapshot(state, note) {
|
|
|
72
161
|
}
|
|
73
162
|
lines.push('');
|
|
74
163
|
|
|
75
|
-
// Decisions Made
|
|
164
|
+
// Decisions Made — inline actual CONTEXT.md content
|
|
76
165
|
lines.push('## Decisions Made');
|
|
77
|
-
|
|
166
|
+
const contextContent = readPhaseContext(state);
|
|
167
|
+
if (contextContent) {
|
|
168
|
+
lines.push(contextContent);
|
|
169
|
+
} else {
|
|
170
|
+
lines.push('- No decisions recorded yet');
|
|
171
|
+
}
|
|
172
|
+
lines.push('');
|
|
173
|
+
|
|
174
|
+
// Current Plan State
|
|
175
|
+
lines.push('## Current Plan State');
|
|
176
|
+
const planState = readCurrentPlanState(state);
|
|
177
|
+
if (planState) {
|
|
178
|
+
lines.push(planState);
|
|
179
|
+
} else {
|
|
180
|
+
lines.push('- No active plan');
|
|
181
|
+
}
|
|
78
182
|
lines.push('');
|
|
79
183
|
|
|
184
|
+
// Active Story/Task
|
|
185
|
+
if (state.stories?.active?.length > 0) {
|
|
186
|
+
const story = state.stories.active[0];
|
|
187
|
+
lines.push('## Active Story');
|
|
188
|
+
lines.push(`- Slug: ${story.slug || 'unknown'}`);
|
|
189
|
+
lines.push(`- Title: ${story.title || story.slug || 'unknown'}`);
|
|
190
|
+
lines.push(`- Step: ${story.step || 'unknown'}`);
|
|
191
|
+
lines.push('');
|
|
192
|
+
}
|
|
193
|
+
if (state.tasks?.active?.length > 0) {
|
|
194
|
+
const task = state.tasks.active[0];
|
|
195
|
+
lines.push('## Active Task');
|
|
196
|
+
lines.push(`- Slug: ${task.slug || 'unknown'}`);
|
|
197
|
+
lines.push(`- Title: ${task.title || task.slug || 'unknown'}`);
|
|
198
|
+
lines.push(`- Step: ${task.step || 'unknown'}`);
|
|
199
|
+
lines.push('');
|
|
200
|
+
}
|
|
201
|
+
|
|
80
202
|
// Conversation Summary
|
|
81
203
|
lines.push('## Conversation Summary');
|
|
82
204
|
lines.push('<!-- Claude: summarize the current conversation context here when presenting to user -->');
|
|
@@ -119,8 +241,6 @@ async function run(args = [], opts = {}) {
|
|
|
119
241
|
if (!fs.existsSync(sessionsDir)) {
|
|
120
242
|
fs.mkdirSync(sessionsDir, { recursive: true });
|
|
121
243
|
}
|
|
122
|
-
const sessionFileName = now.replace(/:/g, '-').replace(/\.\d+Z$/, 'Z') + '.md';
|
|
123
|
-
// Simplify to safe filename
|
|
124
244
|
const safeFileName = now.slice(0, 19).replace(/:/g, '-') + '.md';
|
|
125
245
|
fs.copyFileSync(snapshotPath, path.join(sessionsDir, safeFileName));
|
|
126
246
|
|
|
@@ -56,6 +56,100 @@ function readResearchSummary(brainDir, phaseNumber) {
|
|
|
56
56
|
return fs.readFileSync(summaryPath, 'utf8');
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Check if existing plans in a phase contradict CONTEXT.md decisions.
|
|
61
|
+
* Performs keyword matching between locked decisions/deferred items and plan task content.
|
|
62
|
+
* @param {string} brainDir
|
|
63
|
+
* @param {number} phaseNumber
|
|
64
|
+
* @returns {string|null} Warning message if violations found, null otherwise
|
|
65
|
+
*/
|
|
66
|
+
function checkContextCompliance(brainDir, phaseNumber) {
|
|
67
|
+
const contextContent = readContext(brainDir, phaseNumber);
|
|
68
|
+
if (!contextContent) return null;
|
|
69
|
+
|
|
70
|
+
const phaseDir = findPhaseDir(brainDir, phaseNumber);
|
|
71
|
+
if (!phaseDir) return null;
|
|
72
|
+
|
|
73
|
+
// Read existing plan files
|
|
74
|
+
let planContent = '';
|
|
75
|
+
try {
|
|
76
|
+
const files = fs.readdirSync(phaseDir).filter(f => /^PLAN-\d+\.md$/.test(f));
|
|
77
|
+
for (const f of files) {
|
|
78
|
+
planContent += fs.readFileSync(path.join(phaseDir, f), 'utf8') + '\n';
|
|
79
|
+
}
|
|
80
|
+
} catch { /* no plans yet */ }
|
|
81
|
+
|
|
82
|
+
if (!planContent) return null;
|
|
83
|
+
|
|
84
|
+
const warnings = [];
|
|
85
|
+
const planLower = planContent.toLowerCase();
|
|
86
|
+
|
|
87
|
+
// Parse locked decisions from CONTEXT.md
|
|
88
|
+
const decisionMatch = contextContent.match(/locked\s+decisions?[\s\S]*?(?=##|$)/i);
|
|
89
|
+
if (decisionMatch) {
|
|
90
|
+
const decisions = decisionMatch[0].split('\n').filter(l => l.trim().startsWith('-'));
|
|
91
|
+
for (const decision of decisions) {
|
|
92
|
+
// Extract technology choices from decisions (e.g., "use PostgreSQL", "JWT-based auth")
|
|
93
|
+
const techWords = decision.match(/\b(PostgreSQL|MySQL|MongoDB|Redis|JWT|session|REST|GraphQL|React|Vue|Angular|Express|FastAPI|Django|Laravel)\b/gi);
|
|
94
|
+
if (!techWords) continue;
|
|
95
|
+
|
|
96
|
+
for (const tech of techWords) {
|
|
97
|
+
// Check for contradicting alternatives in plans
|
|
98
|
+
const alternatives = getAlternatives(tech.toLowerCase());
|
|
99
|
+
for (const alt of alternatives) {
|
|
100
|
+
if (planLower.includes(alt.toLowerCase())) {
|
|
101
|
+
warnings.push(`Decision says "${tech}" but plan references "${alt}"`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Parse deferred items from CONTEXT.md
|
|
109
|
+
const deferredMatch = contextContent.match(/deferred[\s\S]*?(?=##|$)/i);
|
|
110
|
+
if (deferredMatch) {
|
|
111
|
+
const deferred = deferredMatch[0].split('\n').filter(l => l.trim().startsWith('-'));
|
|
112
|
+
for (const item of deferred) {
|
|
113
|
+
const keywords = item.replace(/^[\s-]+/, '').split(/\s+/).filter(w => w.length > 4).slice(0, 3);
|
|
114
|
+
for (const kw of keywords) {
|
|
115
|
+
if (planLower.includes(kw.toLowerCase())) {
|
|
116
|
+
warnings.push(`Deferred item mentions "${kw}" but it appears in current plans`);
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (warnings.length === 0) return null;
|
|
124
|
+
return warnings.map(w => `> - ${w}`).join('\n');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Get common alternative technologies for contradiction detection.
|
|
129
|
+
* @param {string} tech
|
|
130
|
+
* @returns {string[]}
|
|
131
|
+
*/
|
|
132
|
+
function getAlternatives(tech) {
|
|
133
|
+
const map = {
|
|
134
|
+
postgresql: ['mongodb', 'mysql', 'sqlite', 'dynamodb'],
|
|
135
|
+
mysql: ['postgresql', 'mongodb', 'sqlite'],
|
|
136
|
+
mongodb: ['postgresql', 'mysql', 'sqlite'],
|
|
137
|
+
redis: ['memcached'],
|
|
138
|
+
jwt: ['session', 'cookie-based', 'oauth'],
|
|
139
|
+
session: ['jwt', 'token-based'],
|
|
140
|
+
rest: ['graphql', 'grpc', 'trpc'],
|
|
141
|
+
graphql: ['rest', 'grpc'],
|
|
142
|
+
react: ['vue', 'angular', 'svelte'],
|
|
143
|
+
vue: ['react', 'angular', 'svelte'],
|
|
144
|
+
angular: ['react', 'vue', 'svelte'],
|
|
145
|
+
express: ['fastify', 'koa', 'hapi'],
|
|
146
|
+
fastapi: ['django', 'flask'],
|
|
147
|
+
django: ['fastapi', 'flask'],
|
|
148
|
+
laravel: ['symfony', 'lumen']
|
|
149
|
+
};
|
|
150
|
+
return map[tech] || [];
|
|
151
|
+
}
|
|
152
|
+
|
|
59
153
|
/**
|
|
60
154
|
* Generate the planner prompt for a single phase.
|
|
61
155
|
* @param {object} phase - Phase data from roadmap
|
|
@@ -174,6 +268,9 @@ function handleSingle(args, brainDir, state) {
|
|
|
174
268
|
|
|
175
269
|
const { prompt, output_dir } = generatePlannerPrompt(phase, brainDir);
|
|
176
270
|
|
|
271
|
+
// Context compliance pre-check: warn if existing plans contradict CONTEXT.md decisions
|
|
272
|
+
const complianceWarning = checkContextCompliance(brainDir, phaseNumber);
|
|
273
|
+
|
|
177
274
|
// Get planner agent metadata and resolve model
|
|
178
275
|
const plannerAgent = getAgent('planner');
|
|
179
276
|
const model = resolveModel('planner', state);
|
|
@@ -185,8 +282,11 @@ function handleSingle(args, brainDir, state) {
|
|
|
185
282
|
plan: 'all'
|
|
186
283
|
});
|
|
187
284
|
|
|
188
|
-
// Append checker loop instruction
|
|
189
|
-
|
|
285
|
+
// Append checker loop instruction + compliance warning
|
|
286
|
+
let checkerInstruction = '\n\n> After plans are generated, plan-checker will validate. Be prepared for revision requests. Max 5 checker iterations before deadlock analysis.';
|
|
287
|
+
if (complianceWarning) {
|
|
288
|
+
checkerInstruction += `\n\n> **CONTEXT COMPLIANCE WARNING:**\n${complianceWarning}`;
|
|
289
|
+
}
|
|
190
290
|
const fullPrompt = prompt + checkerInstruction;
|
|
191
291
|
|
|
192
292
|
// Update state: phase status = "planning"
|
|
@@ -24,6 +24,14 @@ function nextAction(state) {
|
|
|
24
24
|
return '/brain:complete';
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
// Check for active tasks/stories that take priority
|
|
28
|
+
if (state.tasks?.active?.length > 0) {
|
|
29
|
+
return '/brain:new-task --continue';
|
|
30
|
+
}
|
|
31
|
+
if (state.stories?.active?.length > 0) {
|
|
32
|
+
return '/brain:story --continue';
|
|
33
|
+
}
|
|
34
|
+
|
|
27
35
|
// Route based on current phase status
|
|
28
36
|
switch (phase.status) {
|
|
29
37
|
case 'initialized':
|
|
@@ -33,8 +41,11 @@ function nextAction(state) {
|
|
|
33
41
|
case 'discussing':
|
|
34
42
|
case 'discussed':
|
|
35
43
|
return '/brain:plan';
|
|
44
|
+
case 'ready':
|
|
45
|
+
return '/brain:discuss';
|
|
36
46
|
case 'planning':
|
|
37
|
-
|
|
47
|
+
case 'planned':
|
|
48
|
+
return '/brain:plan';
|
|
38
49
|
case 'executing':
|
|
39
50
|
return '/brain:execute';
|
|
40
51
|
case 'executed':
|
|
@@ -44,7 +55,12 @@ function nextAction(state) {
|
|
|
44
55
|
case 'verified':
|
|
45
56
|
return '/brain:complete';
|
|
46
57
|
case 'verification-failed':
|
|
58
|
+
case 'partial':
|
|
47
59
|
return '/brain:execute';
|
|
60
|
+
case 'failed':
|
|
61
|
+
return '/brain:recover';
|
|
62
|
+
case 'paused':
|
|
63
|
+
return '/brain:resume';
|
|
48
64
|
case 'complete':
|
|
49
65
|
return '/brain:complete';
|
|
50
66
|
default:
|
|
@@ -76,6 +76,37 @@ function parseSections(body) {
|
|
|
76
76
|
return sections;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Build list of context files the resumed agent should read.
|
|
81
|
+
* @param {string} brainDir
|
|
82
|
+
* @param {number|string} phaseNumber
|
|
83
|
+
* @returns {string[]} Paths relative to .brain/
|
|
84
|
+
*/
|
|
85
|
+
function buildContextFilesList(brainDir, phaseNumber) {
|
|
86
|
+
const files = [];
|
|
87
|
+
const stateMdPath = path.join(brainDir, 'STATE.md');
|
|
88
|
+
if (fs.existsSync(stateMdPath)) files.push('.brain/STATE.md');
|
|
89
|
+
|
|
90
|
+
if (phaseNumber) {
|
|
91
|
+
const padded = String(phaseNumber).padStart(2, '0');
|
|
92
|
+
const phasesDir = path.join(brainDir, 'phases');
|
|
93
|
+
if (fs.existsSync(phasesDir)) {
|
|
94
|
+
const dirs = fs.readdirSync(phasesDir).filter(d => d.startsWith(padded + '-'));
|
|
95
|
+
if (dirs.length > 0) {
|
|
96
|
+
const phaseDir = path.join(phasesDir, dirs[0]);
|
|
97
|
+
const phaseFiles = fs.readdirSync(phaseDir);
|
|
98
|
+
for (const f of phaseFiles) {
|
|
99
|
+
if (f === 'CONTEXT.md' || f.startsWith('PLAN') || f.toUpperCase().startsWith('SUMMARY')) {
|
|
100
|
+
files.push(`.brain/phases/${dirs[0]}/${f}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return files;
|
|
108
|
+
}
|
|
109
|
+
|
|
79
110
|
/**
|
|
80
111
|
* Run the resume command.
|
|
81
112
|
* @param {string[]} args - CLI arguments
|
|
@@ -179,14 +210,32 @@ async function run(args = [], opts = {}) {
|
|
|
179
210
|
}
|
|
180
211
|
briefingLines.push('');
|
|
181
212
|
}
|
|
213
|
+
if (sections.decisionsMade && sections.decisionsMade !== '- No decisions recorded yet') {
|
|
214
|
+
briefingLines.push(prefix('Decisions:'));
|
|
215
|
+
for (const line of sections.decisionsMade.split('\n').filter(l => l.trim()).slice(0, 10)) {
|
|
216
|
+
briefingLines.push(` ${line}`);
|
|
217
|
+
}
|
|
218
|
+
briefingLines.push('');
|
|
219
|
+
}
|
|
220
|
+
if (sections.currentPlanState && sections.currentPlanState !== '- No active plan') {
|
|
221
|
+
briefingLines.push(prefix('Plan State:'));
|
|
222
|
+
for (const line of sections.currentPlanState.split('\n').filter(l => l.trim())) {
|
|
223
|
+
briefingLines.push(` ${line}`);
|
|
224
|
+
}
|
|
225
|
+
briefingLines.push('');
|
|
226
|
+
}
|
|
182
227
|
if (sections.nextAction) {
|
|
183
228
|
briefingLines.push(prefix(`Next: ${sections.nextAction}`));
|
|
184
229
|
}
|
|
185
230
|
|
|
231
|
+
// Build contextFiles list for the resumed agent to read
|
|
232
|
+
const contextFiles = buildContextFilesList(brainDir, frontmatter.phase);
|
|
233
|
+
|
|
186
234
|
const result = {
|
|
187
235
|
briefing: true,
|
|
188
236
|
snapshot: { frontmatter, sections },
|
|
189
|
-
source: snapshotSource
|
|
237
|
+
source: snapshotSource,
|
|
238
|
+
contextFiles
|
|
190
239
|
};
|
|
191
240
|
|
|
192
241
|
output(result, briefingLines.join('\n'));
|
|
@@ -196,7 +196,13 @@ function handleAnswers(brainDir, state, answersJson) {
|
|
|
196
196
|
|
|
197
197
|
// Update story.json
|
|
198
198
|
const storyMetaPath = path.join(storyDir, 'story.json');
|
|
199
|
-
|
|
199
|
+
let storyMeta;
|
|
200
|
+
try {
|
|
201
|
+
storyMeta = JSON.parse(fs.readFileSync(storyMetaPath, 'utf8'));
|
|
202
|
+
} catch {
|
|
203
|
+
error('Corrupt story.json. Cannot continue story.');
|
|
204
|
+
return { error: 'corrupt-story-meta' };
|
|
205
|
+
}
|
|
200
206
|
storyMeta.status = 'initialized';
|
|
201
207
|
storyMeta.answersReceived = new Date().toISOString();
|
|
202
208
|
fs.writeFileSync(storyMetaPath, JSON.stringify(storyMeta, null, 2));
|
|
@@ -248,7 +254,13 @@ function handleContinue(brainDir, state, values) {
|
|
|
248
254
|
}
|
|
249
255
|
|
|
250
256
|
const storyMetaPath = path.join(storyDir, 'story.json');
|
|
251
|
-
|
|
257
|
+
let storyMeta;
|
|
258
|
+
try {
|
|
259
|
+
storyMeta = JSON.parse(fs.readFileSync(storyMetaPath, 'utf8'));
|
|
260
|
+
} catch {
|
|
261
|
+
error('Corrupt story.json. Cannot continue story.');
|
|
262
|
+
return { error: 'corrupt-story-meta' };
|
|
263
|
+
}
|
|
252
264
|
|
|
253
265
|
// File-existence based step detection
|
|
254
266
|
const hasProjectMd = fs.existsSync(path.join(storyDir, 'PROJECT.md'));
|
|
@@ -752,7 +764,13 @@ function handleComplete(brainDir, state) {
|
|
|
752
764
|
}
|
|
753
765
|
|
|
754
766
|
const storyMetaPath = path.join(storyDir, 'story.json');
|
|
755
|
-
|
|
767
|
+
let storyMeta;
|
|
768
|
+
try {
|
|
769
|
+
storyMeta = JSON.parse(fs.readFileSync(storyMetaPath, 'utf8'));
|
|
770
|
+
} catch {
|
|
771
|
+
error('Corrupt story.json. Cannot complete story.');
|
|
772
|
+
return { error: 'corrupt-story-meta' };
|
|
773
|
+
}
|
|
756
774
|
|
|
757
775
|
// Check if all phases are complete
|
|
758
776
|
const allPhasesComplete = state.phase && Array.isArray(state.phase.phases) &&
|
|
@@ -901,7 +919,13 @@ function handleStatus(brainDir, state) {
|
|
|
901
919
|
}
|
|
902
920
|
|
|
903
921
|
const storyMetaPath = path.join(storyDir, 'story.json');
|
|
904
|
-
|
|
922
|
+
let storyMeta;
|
|
923
|
+
try {
|
|
924
|
+
storyMeta = JSON.parse(fs.readFileSync(storyMetaPath, 'utf8'));
|
|
925
|
+
} catch {
|
|
926
|
+
error('Corrupt story.json. Cannot read story status.');
|
|
927
|
+
return { error: 'corrupt-story-meta' };
|
|
928
|
+
}
|
|
905
929
|
|
|
906
930
|
// Determine step progress
|
|
907
931
|
const steps = [];
|
|
@@ -125,7 +125,10 @@ async function run(args = [], opts = {}) {
|
|
|
125
125
|
// 9. Update .gitignore
|
|
126
126
|
updateGitignore(brainDir);
|
|
127
127
|
|
|
128
|
-
// 10.
|
|
128
|
+
// 10. Clear update notification cache (notification should disappear after update)
|
|
129
|
+
try { fs.unlinkSync(path.join(brainDir, '.update-check.json')); } catch {}
|
|
130
|
+
|
|
131
|
+
// 11. Report
|
|
129
132
|
const isUpgrade = installedVersion !== packageVersion;
|
|
130
133
|
const msg = isUpgrade
|
|
131
134
|
? `Updated brain-dev v${installedVersion} → v${packageVersion}. State preserved. Backup: brain.json.pre-update`
|
package/bin/lib/commands.cjs
CHANGED
|
@@ -293,9 +293,28 @@ function showHelp() {
|
|
|
293
293
|
lines.push('');
|
|
294
294
|
}
|
|
295
295
|
|
|
296
|
+
// Add command comparison for work-initiation commands
|
|
297
|
+
lines.push('Which command to use?');
|
|
298
|
+
lines.push(getCommandComparison());
|
|
299
|
+
lines.push('');
|
|
300
|
+
|
|
296
301
|
return lines.join('\n').trimEnd();
|
|
297
302
|
}
|
|
298
303
|
|
|
304
|
+
/**
|
|
305
|
+
* Get a comparison table for the 3 work-initiation commands.
|
|
306
|
+
* @returns {string} Formatted comparison table
|
|
307
|
+
*/
|
|
308
|
+
function getCommandComparison() {
|
|
309
|
+
return [
|
|
310
|
+
' Command Scope Research Discuss Plan Verify Best For',
|
|
311
|
+
' ------- ----- -------- ------- ---- ------ --------',
|
|
312
|
+
' story Multi-phase Yes Yes Yes Yes New features, milestones',
|
|
313
|
+
' new-task Single task Optional Optional Yes Optional Significant changes',
|
|
314
|
+
' quick Minimal No No Mini No Bug fixes, small tweaks'
|
|
315
|
+
].join('\n');
|
|
316
|
+
}
|
|
317
|
+
|
|
299
318
|
/**
|
|
300
319
|
* Show detailed help for a single unimplemented command.
|
|
301
320
|
* Includes command name, description, usage, args, and status.
|
|
@@ -314,4 +333,4 @@ function showCommandHelp(name) {
|
|
|
314
333
|
return text;
|
|
315
334
|
}
|
|
316
335
|
|
|
317
|
-
module.exports = { COMMANDS, getCommandHelp, showHelp, showCommandHelp };
|
|
336
|
+
module.exports = { COMMANDS, getCommandHelp, showHelp, showCommandHelp, getCommandComparison };
|
package/bin/lib/recovery.cjs
CHANGED
|
@@ -4,7 +4,7 @@ const fs = require('node:fs');
|
|
|
4
4
|
const path = require('node:path');
|
|
5
5
|
const { readLock, isLockStale, clearStaleLock } = require('./lock.cjs');
|
|
6
6
|
const { readLog } = require('./logger.cjs');
|
|
7
|
-
const { readState, writeState } = require('./state.cjs');
|
|
7
|
+
const { readState, writeState, VALID_PHASE_STATUSES } = require('./state.cjs');
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Analyze the JSONL execution log for a crashed phase.
|
|
@@ -141,8 +141,7 @@ function verifyStateConsistency(brainDir, state) {
|
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
// Check: status is a known value
|
|
144
|
-
|
|
145
|
-
if (phase.status && !validStatuses.includes(phase.status)) {
|
|
144
|
+
if (phase.status && !VALID_PHASE_STATUSES.includes(phase.status)) {
|
|
146
145
|
issues.push({
|
|
147
146
|
type: 'invalid-status',
|
|
148
147
|
description: `Phase status "${phase.status}" is not a recognized value`,
|
package/bin/lib/state.cjs
CHANGED
|
@@ -499,11 +499,23 @@ function today() {
|
|
|
499
499
|
return new Date().toISOString().slice(0, 10);
|
|
500
500
|
}
|
|
501
501
|
|
|
502
|
+
/**
|
|
503
|
+
* Canonical list of valid phase statuses.
|
|
504
|
+
* Used by recovery.cjs, progress.cjs, and health.cjs for validation.
|
|
505
|
+
*/
|
|
506
|
+
const VALID_PHASE_STATUSES = [
|
|
507
|
+
'initialized', 'pending', 'mapped', 'ready', 'discussing', 'discussed',
|
|
508
|
+
'planning', 'planned', 'executing', 'executed',
|
|
509
|
+
'verifying', 'verified', 'verification-failed',
|
|
510
|
+
'partial', 'failed', 'paused', 'complete'
|
|
511
|
+
];
|
|
512
|
+
|
|
502
513
|
module.exports = {
|
|
503
514
|
atomicWriteSync,
|
|
504
515
|
readState,
|
|
505
516
|
writeState,
|
|
506
517
|
generateStateMd,
|
|
507
518
|
createDefaultState,
|
|
508
|
-
migrateState
|
|
519
|
+
migrateState,
|
|
520
|
+
VALID_PHASE_STATUSES
|
|
509
521
|
};
|
package/bin/lib/stuck.cjs
CHANGED
|
@@ -104,9 +104,74 @@ function checkTimeouts(brainDir, state) {
|
|
|
104
104
|
};
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
// Check for loop detection (stuck even without timeout)
|
|
108
|
+
const loop = detectStuckLoop(brainDir, state.phase?.current || 1);
|
|
109
|
+
if (loop.looping) {
|
|
110
|
+
return {
|
|
111
|
+
tier: 'idle',
|
|
112
|
+
elapsedMinutes,
|
|
113
|
+
idleMinutes,
|
|
114
|
+
message: `LOOP DETECTED: ${loop.details}. Agent is repeating the same action without progress.`
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
107
118
|
return { tier: 'none', elapsedMinutes, idleMinutes, message: null };
|
|
108
119
|
}
|
|
109
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Detect stuck loops: same plan spawned 3+ times, or same error repeated 3+ times.
|
|
123
|
+
* @param {string} brainDir
|
|
124
|
+
* @param {number} phaseNumber
|
|
125
|
+
* @returns {{ looping: boolean, loopType: 'spawn-repeat'|'error-repeat'|null, details: string|null }}
|
|
126
|
+
*/
|
|
127
|
+
function detectStuckLoop(brainDir, phaseNumber) {
|
|
128
|
+
const events = readLog(brainDir, phaseNumber);
|
|
129
|
+
|
|
130
|
+
// Check 1: Same plan spawned 3+ times without passing
|
|
131
|
+
const spawnsByPlan = {};
|
|
132
|
+
const passedPlans = new Set();
|
|
133
|
+
|
|
134
|
+
for (const event of events) {
|
|
135
|
+
if (event.type === 'spawn' && event.agent === 'executor' && event.plan) {
|
|
136
|
+
spawnsByPlan[event.plan] = (spawnsByPlan[event.plan] || 0) + 1;
|
|
137
|
+
}
|
|
138
|
+
if (event.type === 'spot-check' && event.passed && event.plan) {
|
|
139
|
+
passedPlans.add(event.plan);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
for (const [plan, count] of Object.entries(spawnsByPlan)) {
|
|
144
|
+
if (count >= 3 && !passedPlans.has(plan)) {
|
|
145
|
+
return {
|
|
146
|
+
looping: true,
|
|
147
|
+
loopType: 'spawn-repeat',
|
|
148
|
+
details: `Plan ${plan} attempted ${count} times without passing spot-check`
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Check 2: Same error message repeated 3+ times
|
|
154
|
+
const errorMessages = {};
|
|
155
|
+
for (const event of events) {
|
|
156
|
+
if (event.type === 'error' && event.message) {
|
|
157
|
+
const key = event.message.slice(0, 100); // normalize by truncating
|
|
158
|
+
errorMessages[key] = (errorMessages[key] || 0) + 1;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
for (const [msg, count] of Object.entries(errorMessages)) {
|
|
163
|
+
if (count >= 3) {
|
|
164
|
+
return {
|
|
165
|
+
looping: true,
|
|
166
|
+
loopType: 'error-repeat',
|
|
167
|
+
details: `Same error repeated ${count} times: "${msg.slice(0, 60)}..."`
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return { looping: false, loopType: null, details: null };
|
|
173
|
+
}
|
|
174
|
+
|
|
110
175
|
/**
|
|
111
176
|
* Check for execution non-convergence.
|
|
112
177
|
* Detects: same plan attempted 3+ times, repeated spawns without spot-check pass.
|
|
@@ -162,6 +227,7 @@ function captureDiagnostics(brainDir, phaseNumber, stuckInfo) {
|
|
|
162
227
|
|
|
163
228
|
const events = tailLog(brainDir, phaseNumber, null);
|
|
164
229
|
const convergence = checkExecutionConvergence(brainDir, phaseNumber);
|
|
230
|
+
const loop = detectStuckLoop(brainDir, phaseNumber);
|
|
165
231
|
|
|
166
232
|
const content = `---
|
|
167
233
|
phase: ${phaseNumber}
|
|
@@ -183,6 +249,10 @@ timeout_tier: ${stuckInfo.tier}
|
|
|
183
249
|
- Converging: ${convergence.converging}
|
|
184
250
|
${convergence.reason ? `- Issue: ${convergence.reason}` : ''}
|
|
185
251
|
|
|
252
|
+
## Loop Detection
|
|
253
|
+
- Looping: ${loop.looping}
|
|
254
|
+
${loop.details ? `- Type: ${loop.loopType}\n- Details: ${loop.details}` : ''}
|
|
255
|
+
|
|
186
256
|
## Last ${events.length} Events
|
|
187
257
|
${events.map(e => `- ${e.timestamp || '?'} [${e.type || '?'}] ${JSON.stringify(e)}`).join('\n')}
|
|
188
258
|
|
|
@@ -260,6 +330,7 @@ function updateStuckBridge(brainDir, state) {
|
|
|
260
330
|
module.exports = {
|
|
261
331
|
detectProgress,
|
|
262
332
|
checkTimeouts,
|
|
333
|
+
detectStuckLoop,
|
|
263
334
|
checkExecutionConvergence,
|
|
264
335
|
captureDiagnostics,
|
|
265
336
|
buildWrapUpInstructions,
|
package/bin/templates/discuss.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
You are facilitating a gray-area discussion for **Phase {{phase_number}}: {{phase_name}}**.
|
|
4
4
|
|
|
5
|
+
## Technology Context
|
|
6
|
+
{{stack_expertise}}
|
|
7
|
+
|
|
8
|
+
When discussing implementation choices, consider the framework patterns above.
|
|
9
|
+
|
|
5
10
|
## Phase Context
|
|
6
11
|
|
|
7
12
|
**Goal:** {{phase_goal}}
|
|
@@ -27,16 +27,50 @@ Read the plan file above for the full task list and requirements.
|
|
|
27
27
|
- GREEN: Write minimal code to pass
|
|
28
28
|
- REFACTOR: Clean up while keeping tests green
|
|
29
29
|
|
|
30
|
-
2. **
|
|
30
|
+
2. **NEVER commit broken code:** Run all relevant tests before every commit. If tests fail, fix them FIRST. Only commit when tests are GREEN. Do not commit TDD RED phase — only commit after GREEN or REFACTOR.
|
|
31
31
|
|
|
32
|
-
3. **
|
|
32
|
+
3. **Sequential execution:** Execute tasks one at a time, in order. Do not parallelize. Complete one plan before moving to the next.
|
|
33
33
|
|
|
34
|
-
4. **
|
|
34
|
+
4. **Commit after each task:** Use per-task atomic commit format (see Commit Format below).
|
|
35
|
+
|
|
36
|
+
5. **Retry on failure:** If a task fails, retry once. If the retry also fails, output `## EXECUTION FAILED` with a structured failure block.
|
|
37
|
+
|
|
38
|
+
### TDD Escape Hatch
|
|
39
|
+
|
|
40
|
+
If you write a failing test (RED phase) and cannot make it pass after **3 implementation attempts**:
|
|
41
|
+
|
|
42
|
+
1. Mark the test as `.skip()` or `{ skip: true }` with a TODO comment explaining why
|
|
43
|
+
2. Log the skipped test in SUMMARY.md Deviations section with category `tdd-escape`
|
|
44
|
+
3. Proceed to the next task — do NOT loop indefinitely
|
|
45
|
+
4. The verifier will flag this as a gap, which is better than an infinite loop
|
|
46
|
+
|
|
47
|
+
### Context Window Awareness
|
|
48
|
+
|
|
49
|
+
Monitor your context usage. If you estimate you have used more than **60% of the context window**:
|
|
50
|
+
|
|
51
|
+
1. Commit all current completed work
|
|
52
|
+
2. Write a partial SUMMARY.md with `completed: "partial"` in frontmatter
|
|
53
|
+
3. Output `## EXECUTION PARTIAL` with the list of completed tasks and remaining tasks
|
|
54
|
+
4. Do NOT attempt to continue with exhausted context — partial progress is better than a crash
|
|
35
55
|
|
|
36
56
|
## Deviation Rules
|
|
37
57
|
|
|
38
58
|
When executing, you will encounter issues not anticipated by the plan. Apply these rules:
|
|
39
59
|
|
|
60
|
+
### Deviation Decision Tree
|
|
61
|
+
|
|
62
|
+
When you encounter an issue not anticipated by the plan, use this decision tree:
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
Is the change confined to the CURRENT file only?
|
|
66
|
+
├─ YES: Does it change any export signatures (function names, parameters, return types)?
|
|
67
|
+
│ ├─ NO → AUTO-FIX (fix immediately, log in Deviations)
|
|
68
|
+
│ └─ YES → ESCALATE (other modules depend on this signature)
|
|
69
|
+
└─ NO: Does it affect files OUTSIDE this plan's files_modified list?
|
|
70
|
+
├─ NO → AUTO-FIX (still within plan scope)
|
|
71
|
+
└─ YES → ESCALATE (cross-plan impact)
|
|
72
|
+
```
|
|
73
|
+
|
|
40
74
|
### Auto-fix Scope (fix immediately, no permission needed)
|
|
41
75
|
|
|
42
76
|
- **Test failures:** Fix broken assertions, update snapshots, correct test setup
|
package/bin/templates/planner.md
CHANGED
|
@@ -20,6 +20,32 @@ File paths, naming, and testing approach must match the detected stack.
|
|
|
20
20
|
|
|
21
21
|
{{research_summary}}
|
|
22
22
|
|
|
23
|
+
## Requirement Quality Gate
|
|
24
|
+
|
|
25
|
+
Before planning, validate each requirement from `{{phase_requirements}}`. Reject and escalate requirements that are:
|
|
26
|
+
|
|
27
|
+
**Vague** — cannot be turned into a testable truth:
|
|
28
|
+
- BAD: "Make authentication work" → too vague, what does "work" mean?
|
|
29
|
+
- BAD: "Improve performance" → no measurable target
|
|
30
|
+
- BAD: "Handle errors properly" → no definition of "properly"
|
|
31
|
+
- ESCALATE: Output `## PLANNING BLOCKED` with the vague requirement and ask for specifics
|
|
32
|
+
|
|
33
|
+
**Circular** — restates the goal without adding implementation detail:
|
|
34
|
+
- BAD: "Implement the user management feature" when the phase goal IS user management
|
|
35
|
+
- FIX: Break into concrete sub-requirements (CRUD operations, validation rules, auth integration)
|
|
36
|
+
|
|
37
|
+
**Contradictory** — conflicts with another requirement or a locked decision from CONTEXT.md:
|
|
38
|
+
- BAD: REQ-03 says "use REST API" but REQ-07 says "use GraphQL for all endpoints"
|
|
39
|
+
- ESCALATE: Output `## PLANNING BLOCKED` listing both contradicting requirements
|
|
40
|
+
|
|
41
|
+
If a requirement passes the quality gate, proceed. If 2+ requirements fail, output PLANNING BLOCKED and do not generate plans.
|
|
42
|
+
|
|
43
|
+
## Scope Guardrails
|
|
44
|
+
|
|
45
|
+
- Each plan must modify **no more than 8 files**. If a plan needs more, split it into multiple plans.
|
|
46
|
+
- Each plan should have **2-3 tasks**. If you need 5+ tasks, the plan scope is too wide — split it.
|
|
47
|
+
- If the phase has 10+ requirements, create multiple plans with clear requirement ownership rather than one mega-plan.
|
|
48
|
+
|
|
23
49
|
## Output Format: Brain PLAN.md
|
|
24
50
|
|
|
25
51
|
Create PLAN files at: `{{output_dir}}/PLAN-{nn}.md`
|
|
@@ -134,6 +160,42 @@ Use **goal-backward** approach to derive must_haves:
|
|
|
134
160
|
4. **For each task:** define behavior first (TDD), then action
|
|
135
161
|
5. **Verify completeness:** every must_have is covered by at least one plan's tasks
|
|
136
162
|
|
|
163
|
+
### must_haves Quality Examples
|
|
164
|
+
|
|
165
|
+
**BAD (vague, untestable):**
|
|
166
|
+
```yaml
|
|
167
|
+
truths:
|
|
168
|
+
- "Authentication works correctly"
|
|
169
|
+
- "API handles errors"
|
|
170
|
+
- "Database integration is complete"
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**GOOD (specific, testable, verifiable):**
|
|
174
|
+
```yaml
|
|
175
|
+
truths:
|
|
176
|
+
- "POST /auth/login with valid email+password returns 200 with JWT token containing user_id claim"
|
|
177
|
+
- "All API endpoints return {error: string, code: number} JSON on 4xx/5xx responses"
|
|
178
|
+
- "User.findById(id) returns a User object with name, email, role fields from PostgreSQL"
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
The difference: good truths can be directly turned into test assertions. If you cannot write `assert.equal(...)` or `expect(...)` from a truth, it is too vague.
|
|
182
|
+
|
|
183
|
+
## Planning Failures
|
|
184
|
+
|
|
185
|
+
If you cannot create a valid plan after **2 attempts** (e.g., checker keeps rejecting, requirements are impossible, dependencies unresolvable), output:
|
|
186
|
+
|
|
187
|
+
```markdown
|
|
188
|
+
## PLANNING BLOCKED
|
|
189
|
+
|
|
190
|
+
**Phase:** {{phase_number}} - {{phase_name}}
|
|
191
|
+
**Reason:** [requirement_unclear | dependency_unresolved | scope_too_large | contradictory_requirements]
|
|
192
|
+
**Details:** [specific explanation of what is blocking]
|
|
193
|
+
**Missing:** [what information or decision is needed to unblock]
|
|
194
|
+
**Suggestion:** [recommended next action -- e.g., "/brain:discuss to clarify requirements"]
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Do NOT produce incomplete or low-quality plans. Blocking with clear information is better than a plan that will fail during execution.
|
|
198
|
+
|
|
137
199
|
## Enriched SUMMARY.md
|
|
138
200
|
|
|
139
201
|
After each plan executes, the executor creates a SUMMARY.md with:
|
|
@@ -19,6 +19,20 @@ During verification, check:
|
|
|
19
19
|
|
|
20
20
|
Use these results when evaluating Level 2 (Substantive) checks. Blockers MUST be reported as verification failures. Warnings should be noted but do not block.
|
|
21
21
|
|
|
22
|
+
### Anti-Pattern Response Format
|
|
23
|
+
|
|
24
|
+
For each blocker-level anti-pattern found, output a structured remediation block:
|
|
25
|
+
|
|
26
|
+
```markdown
|
|
27
|
+
**BLOCKER:** [anti-pattern name]
|
|
28
|
+
- **File:** [path/to/file.cjs]
|
|
29
|
+
- **Lines:** [line range, e.g., 45-52]
|
|
30
|
+
- **Issue:** [specific description of what was found]
|
|
31
|
+
- **Fix:** [one-sentence concrete fix suggestion]
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Warnings are collected but do not affect the score. Blockers must be resolved before status can be "passed".
|
|
35
|
+
|
|
22
36
|
## 3-Level Verification
|
|
23
37
|
|
|
24
38
|
For each artifact in must_haves, perform these automated checks:
|
|
@@ -30,10 +44,17 @@ For each artifact in must_haves, perform these automated checks:
|
|
|
30
44
|
### Level 2: Substantive
|
|
31
45
|
- File has real implementation (not stub)
|
|
32
46
|
- No TODO-only files, no empty functions, no log-only handlers
|
|
33
|
-
- Minimum reasonable line count for the artifact type
|
|
34
47
|
- Anti-pattern scan: count TODO/FIXME, placeholder content, empty returns
|
|
35
48
|
- Cross-reference anti-pattern scan results above for blockers
|
|
36
49
|
|
|
50
|
+
**Minimum line count thresholds:**
|
|
51
|
+
- Functions/methods: at least **5 lines** of implementation (not counting braces/signature)
|
|
52
|
+
- Classes: at least **20 lines** total
|
|
53
|
+
- Modules/files: at least **30 lines** (excluding imports and empty lines)
|
|
54
|
+
- Test files: at least **1 test per exported function** in the module under test
|
|
55
|
+
|
|
56
|
+
If a file exports N functions, at least N-1 must have implementation beyond a single return statement. A function that only does `return null` or `return undefined` is a stub, not an implementation.
|
|
57
|
+
|
|
37
58
|
### Level 3: Wired (Enhanced Key Link Verification)
|
|
38
59
|
- File is imported/used where expected
|
|
39
60
|
- Check key_links: from-file imports/references to-file with specified pattern
|
|
@@ -88,13 +109,24 @@ Where:
|
|
|
88
109
|
- **verified_count** = number of must_haves that pass all applicable levels
|
|
89
110
|
- **total_count** = total number of must_haves (truths + artifacts + key_links)
|
|
90
111
|
|
|
112
|
+
## Verification Priority
|
|
113
|
+
|
|
114
|
+
Check must_haves in this order (most important first):
|
|
115
|
+
|
|
116
|
+
1. **Truths** — behavioral invariants. If a truth fails, the feature is broken.
|
|
117
|
+
2. **Artifacts** — files that must exist with correct content. If an artifact fails, the deliverable is incomplete.
|
|
118
|
+
3. **Key Links** — wiring between files. If a key link fails, integration is broken.
|
|
119
|
+
|
|
120
|
+
A truth failure is more severe than an artifact failure. Use this priority when determining status.
|
|
121
|
+
|
|
91
122
|
### Status Determination
|
|
92
123
|
|
|
93
124
|
Based on the score:
|
|
94
125
|
|
|
95
126
|
- **passed** (100%): All must_haves verified at all levels. Phase/plan deliverables are complete.
|
|
96
|
-
- **
|
|
97
|
-
- **
|
|
127
|
+
- **partial** (70-99%): Most must_haves verified. All truths pass but some artifacts or key_links have gaps. List specific gaps with remediation steps. This status tells the executor exactly what to fix without replanning the entire phase.
|
|
128
|
+
- **gaps_found** (<70%, or any truth failure): Critical must_haves failed verification. The phase deliverables are fundamentally incomplete. List all gaps with remediation steps.
|
|
129
|
+
- **human_needed**: Automated checks passed (score ≥70%) but items require human verification (visual, interactive, UX). Human gate pending.
|
|
98
130
|
|
|
99
131
|
## Output Format
|
|
100
132
|
|
|
@@ -107,7 +139,7 @@ Start the file with YAML frontmatter containing machine-parseable metadata:
|
|
|
107
139
|
```yaml
|
|
108
140
|
---
|
|
109
141
|
phase: [phase number]
|
|
110
|
-
status: passed | gaps_found | human_needed
|
|
142
|
+
status: passed | partial | gaps_found | human_needed
|
|
111
143
|
score: [verified]/[total]
|
|
112
144
|
must_haves_verified: [count]
|
|
113
145
|
must_haves_total: [count]
|