brain-dev 1.2.7 → 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.
@@ -242,10 +242,19 @@ function buildInstructions(runbook, brainDir) {
242
242
  lines.push('');
243
243
  }
244
244
 
245
- lines.push('### Error Handling');
246
- lines.push('- If any step fails, retry once. If it fails again, run: `npx brain-dev auto --stop`');
247
- lines.push('- Budget check: run `npx brain-dev progress --cost` between phases');
248
- lines.push('- Timeout check: run `npx brain-dev progress --stuck` if a step takes too long');
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
- const taskMeta = JSON.parse(fs.readFileSync(path.join(taskDir, 'task.json'), 'utf8'));
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
- const taskMeta = JSON.parse(fs.readFileSync(path.join(taskDir, 'task.json'), 'utf8'));
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
- lines.push('- See .brain/brain.json for full state');
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
- const checkerInstruction = '\n\n> After plans are generated, plan-checker will validate. Be prepared for revision requests. Max 5 checker iterations before deadlock analysis.';
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
- return '/brain:execute';
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
- const storyMeta = JSON.parse(fs.readFileSync(storyMetaPath, 'utf8'));
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
- const storyMeta = JSON.parse(fs.readFileSync(storyMetaPath, 'utf8'));
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
- const storyMeta = JSON.parse(fs.readFileSync(storyMetaPath, 'utf8'));
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
- const storyMeta = JSON.parse(fs.readFileSync(storyMetaPath, 'utf8'));
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 = [];
@@ -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 };
@@ -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
- const validStatuses = ['initialized', 'mapped', 'planned', 'executing', 'completed', 'paused', 'failed', 'partial'];
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,
@@ -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. **Sequential execution:** Execute tasks one at a time, in order. Do not parallelize. Complete one plan before moving to the next.
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. **Commit after each task:** Use per-task atomic commit format (see Commit Format below).
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. **Retry on failure:** If a task fails, retry once. If the retry also fails, output `## EXECUTION FAILED` with a structured failure block.
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
@@ -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:
@@ -2,6 +2,9 @@
2
2
 
3
3
  You are a {{focus_area}} researcher for a new software project.
4
4
 
5
+ ## Stack Context
6
+ {{stack_expertise}}
7
+
5
8
  ## Project Context
6
9
 
7
10
  **Project:** {{project_description}}
@@ -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
- - **gaps_found** (<100%): Some must_haves failed verification. List specific gaps with remediation steps.
97
- - **human_needed**: Some items require human verification (visual, interactive, UX). Automated checks passed but human gate pending.
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]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brain-dev",
3
- "version": "1.2.7",
3
+ "version": "2.0.0",
4
4
  "description": "AI-powered development workflow orchestrator",
5
5
  "author": "halilcosdu",
6
6
  "license": "MIT",