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,447 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { readState, writeState } = require('../state.cjs');
|
|
6
|
+
const { loadTemplate, interpolate } = require('../templates.cjs');
|
|
7
|
+
const { resolveModel } = require('../agents.cjs');
|
|
8
|
+
const { logEvent } = require('../logger.cjs');
|
|
9
|
+
const { output, error, success } = require('../core.cjs');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Generate a URL-safe slug from a description.
|
|
13
|
+
* @param {string} text
|
|
14
|
+
* @returns {string}
|
|
15
|
+
*/
|
|
16
|
+
function generateSlug(text) {
|
|
17
|
+
return text
|
|
18
|
+
.toLowerCase()
|
|
19
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
20
|
+
.replace(/^-+|-+$/g, '')
|
|
21
|
+
.substring(0, 40);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Calculate the next quick task number by scanning .brain/quick/.
|
|
26
|
+
* @param {string} quickDir
|
|
27
|
+
* @returns {number}
|
|
28
|
+
*/
|
|
29
|
+
function calculateNextNum(quickDir) {
|
|
30
|
+
try {
|
|
31
|
+
const existing = fs.readdirSync(quickDir)
|
|
32
|
+
.filter(f => /^\d+-/.test(f))
|
|
33
|
+
.map(f => parseInt(f.split('-')[0], 10))
|
|
34
|
+
.filter(n => !isNaN(n));
|
|
35
|
+
return existing.length > 0 ? Math.max(...existing) + 1 : 1;
|
|
36
|
+
} catch {
|
|
37
|
+
return 1;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Find a quick task directory by number.
|
|
43
|
+
* @param {string} brainDir
|
|
44
|
+
* @param {number} taskNum
|
|
45
|
+
* @returns {string|null}
|
|
46
|
+
*/
|
|
47
|
+
function findTaskDir(brainDir, taskNum) {
|
|
48
|
+
const quickDir = path.join(brainDir, 'quick');
|
|
49
|
+
if (!fs.existsSync(quickDir)) return null;
|
|
50
|
+
const match = fs.readdirSync(quickDir).find(d => d.startsWith(`${taskNum}-`));
|
|
51
|
+
return match ? path.join(quickDir, match) : null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Parse --task N from args.
|
|
56
|
+
* @param {string[]} args
|
|
57
|
+
* @returns {number|null}
|
|
58
|
+
*/
|
|
59
|
+
function getTaskNum(args) {
|
|
60
|
+
const idx = args.indexOf('--task');
|
|
61
|
+
if (idx >= 0 && args[idx + 1]) {
|
|
62
|
+
const num = parseInt(args[idx + 1], 10);
|
|
63
|
+
return isNaN(num) ? null : num;
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Run the quick command.
|
|
70
|
+
*
|
|
71
|
+
* Steps (each triggered by flags):
|
|
72
|
+
* (default) → Init + planner instructions
|
|
73
|
+
* --execute --task N → Executor instructions
|
|
74
|
+
* --verify --task N → Verifier instructions (--full only)
|
|
75
|
+
* --complete --task N → STATE.md update + commit instructions
|
|
76
|
+
*
|
|
77
|
+
* @param {string[]} args
|
|
78
|
+
* @param {object} [opts]
|
|
79
|
+
*/
|
|
80
|
+
async function run(args = [], opts = {}) {
|
|
81
|
+
const brainDir = opts.brainDir || path.join(process.cwd(), '.brain');
|
|
82
|
+
const state = readState(brainDir);
|
|
83
|
+
|
|
84
|
+
if (!state) {
|
|
85
|
+
error("No brain state found. Run 'brain-dev init' first.");
|
|
86
|
+
return { error: 'no-state' };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!state.project || !state.project.initialized) {
|
|
90
|
+
error("Project not initialized. Run '/brain:new-project' first.");
|
|
91
|
+
return { error: 'not-initialized' };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Route to step handlers
|
|
95
|
+
if (args.includes('--complete')) {
|
|
96
|
+
return handleComplete(args, brainDir, state);
|
|
97
|
+
}
|
|
98
|
+
if (args.includes('--verify')) {
|
|
99
|
+
return handleVerify(args, brainDir, state);
|
|
100
|
+
}
|
|
101
|
+
if (args.includes('--execute')) {
|
|
102
|
+
return handleExecute(args, brainDir, state);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Default: init + planner
|
|
106
|
+
return handleInit(args, brainDir, state);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Step 1-2: Parse args, create task dir, generate planner instructions.
|
|
111
|
+
*/
|
|
112
|
+
function handleInit(args, brainDir, state) {
|
|
113
|
+
const isFull = args.includes('--full');
|
|
114
|
+
|
|
115
|
+
// Extract description (everything except flags)
|
|
116
|
+
const desc = args.filter(a => !a.startsWith('--')).join(' ').trim();
|
|
117
|
+
|
|
118
|
+
if (!desc) {
|
|
119
|
+
const humanText = [
|
|
120
|
+
'[brain] Quick Task',
|
|
121
|
+
'',
|
|
122
|
+
'IMPORTANT: Use AskUserQuestion to ask what the user wants to do.',
|
|
123
|
+
'Then run: npx brain-dev quick "description here"'
|
|
124
|
+
].join('\n');
|
|
125
|
+
|
|
126
|
+
const result = {
|
|
127
|
+
action: 'ask-description',
|
|
128
|
+
tool: 'AskUserQuestion',
|
|
129
|
+
question: { question: 'What do you want to do?', header: 'Quick Task', options: [
|
|
130
|
+
{ label: 'Fix a bug', description: 'Fix an existing issue or error' },
|
|
131
|
+
{ label: 'Add a feature', description: 'Add new functionality' },
|
|
132
|
+
{ label: 'Refactor', description: 'Improve code quality without changing behavior' },
|
|
133
|
+
{ label: 'Update docs', description: 'Update documentation or README' }
|
|
134
|
+
], multiSelect: false },
|
|
135
|
+
instruction: 'After getting description, run: npx brain-dev quick "user description here"'
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
output(result, humanText);
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Create quick task directory
|
|
143
|
+
const quickDir = path.join(brainDir, 'quick');
|
|
144
|
+
fs.mkdirSync(quickDir, { recursive: true });
|
|
145
|
+
const nextNum = calculateNextNum(quickDir);
|
|
146
|
+
const slug = generateSlug(desc);
|
|
147
|
+
const taskDir = path.join(quickDir, `${nextNum}-${slug}`);
|
|
148
|
+
fs.mkdirSync(taskDir, { recursive: true });
|
|
149
|
+
|
|
150
|
+
// Save task metadata
|
|
151
|
+
const taskMeta = { num: nextNum, slug, description: desc, full: isFull, created: new Date().toISOString() };
|
|
152
|
+
fs.writeFileSync(path.join(taskDir, 'task.json'), JSON.stringify(taskMeta, null, 2));
|
|
153
|
+
|
|
154
|
+
// Generate planner prompt using existing template + quick constraints
|
|
155
|
+
const template = loadTemplate('planner');
|
|
156
|
+
const prompt = interpolate(template, {
|
|
157
|
+
phase_number: nextNum,
|
|
158
|
+
phase_name: desc,
|
|
159
|
+
phase_goal: desc,
|
|
160
|
+
phase_requirements: 'Quick task — single focused deliverable',
|
|
161
|
+
phase_depends_on: 'None',
|
|
162
|
+
context_decisions: '_Quick mode: no CONTEXT.md required._',
|
|
163
|
+
research_summary: '_Quick mode: no research phase._',
|
|
164
|
+
output_dir: taskDir,
|
|
165
|
+
phase_number_padded: String(nextNum).padStart(3, '0'),
|
|
166
|
+
phase_slug: slug
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const quickConstraints = [
|
|
170
|
+
'',
|
|
171
|
+
'## Quick Mode Constraints',
|
|
172
|
+
'',
|
|
173
|
+
'- Create a SINGLE plan file: PLAN-1.md',
|
|
174
|
+
'- Maximum 1-2 focused tasks',
|
|
175
|
+
'- Target ~30% context budget (keep it small and focused)',
|
|
176
|
+
'- No research or context gathering needed',
|
|
177
|
+
'- Task should be atomic and self-contained',
|
|
178
|
+
isFull ? '- MUST include must_haves in frontmatter (truths, artifacts, key_links)' : '',
|
|
179
|
+
isFull ? '- Each task MUST have files, action, verify, done fields' : '',
|
|
180
|
+
'',
|
|
181
|
+
`Write plan to: ${path.join(taskDir, 'PLAN-1.md')}`,
|
|
182
|
+
''
|
|
183
|
+
].filter(Boolean).join('\n');
|
|
184
|
+
|
|
185
|
+
const fullPrompt = prompt + quickConstraints;
|
|
186
|
+
const model = resolveModel('planner', state);
|
|
187
|
+
|
|
188
|
+
logEvent(brainDir, 0, { type: 'quick-init', task: nextNum, description: desc, full: isFull });
|
|
189
|
+
|
|
190
|
+
const humanText = [
|
|
191
|
+
`[brain] Quick Task ${nextNum}: ${desc}`,
|
|
192
|
+
isFull ? '[brain] Full mode: plan checking + verification enabled' : '',
|
|
193
|
+
`[brain] Directory: ${taskDir}`,
|
|
194
|
+
'',
|
|
195
|
+
'IMPORTANT: Use the Agent tool (subagent_type: "brain-planner") to spawn the planner.',
|
|
196
|
+
'Do NOT plan the task yourself.',
|
|
197
|
+
'',
|
|
198
|
+
`After planner completes, run: npx brain-dev quick --execute --task ${nextNum}`,
|
|
199
|
+
'',
|
|
200
|
+
fullPrompt
|
|
201
|
+
].filter(Boolean).join('\n');
|
|
202
|
+
|
|
203
|
+
const result = {
|
|
204
|
+
action: 'spawn-quick-planner',
|
|
205
|
+
task: nextNum,
|
|
206
|
+
taskDir,
|
|
207
|
+
slug,
|
|
208
|
+
isFull,
|
|
209
|
+
prompt: fullPrompt,
|
|
210
|
+
model,
|
|
211
|
+
nextAction: `npx brain-dev quick --execute --task ${nextNum}`
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
output(result, humanText);
|
|
215
|
+
return result;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Step 5: Generate executor instructions for a quick task.
|
|
220
|
+
*/
|
|
221
|
+
function handleExecute(args, brainDir, state) {
|
|
222
|
+
const taskNum = getTaskNum(args);
|
|
223
|
+
if (!taskNum) {
|
|
224
|
+
error('--execute requires --task <number>');
|
|
225
|
+
return { error: 'missing-task-number' };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const taskDir = findTaskDir(brainDir, taskNum);
|
|
229
|
+
if (!taskDir) {
|
|
230
|
+
error(`Quick task ${taskNum} not found.`);
|
|
231
|
+
return { error: 'task-not-found' };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Read plan
|
|
235
|
+
const planPath = path.join(taskDir, 'PLAN-1.md');
|
|
236
|
+
if (!fs.existsSync(planPath)) {
|
|
237
|
+
error(`Plan not found for task ${taskNum}. Run quick task init first.`);
|
|
238
|
+
return { error: 'no-plan' };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const planContent = fs.readFileSync(planPath, 'utf8');
|
|
242
|
+
const summaryPath = path.join(taskDir, 'SUMMARY-1.md');
|
|
243
|
+
|
|
244
|
+
// Load executor template
|
|
245
|
+
const template = loadTemplate('executor');
|
|
246
|
+
const prompt = interpolate(template, {
|
|
247
|
+
plan_path: planPath,
|
|
248
|
+
summary_path: summaryPath,
|
|
249
|
+
plan_content: planContent,
|
|
250
|
+
phase: `Q${taskNum}`,
|
|
251
|
+
plan_number: '1',
|
|
252
|
+
subsystem: `quick-${taskNum}`
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const model = resolveModel('executor', state);
|
|
256
|
+
|
|
257
|
+
// Read task metadata for --full check
|
|
258
|
+
let isFull = false;
|
|
259
|
+
try {
|
|
260
|
+
const meta = JSON.parse(fs.readFileSync(path.join(taskDir, 'task.json'), 'utf8'));
|
|
261
|
+
isFull = meta.full === true;
|
|
262
|
+
} catch { /* ignore */ }
|
|
263
|
+
|
|
264
|
+
const nextCmd = isFull
|
|
265
|
+
? `npx brain-dev quick --verify --task ${taskNum}`
|
|
266
|
+
: `npx brain-dev quick --complete --task ${taskNum}`;
|
|
267
|
+
|
|
268
|
+
logEvent(brainDir, 0, { type: 'quick-execute', task: taskNum });
|
|
269
|
+
|
|
270
|
+
const humanText = [
|
|
271
|
+
`[brain] Executing Quick Task ${taskNum}`,
|
|
272
|
+
`[brain] Plan: ${planPath}`,
|
|
273
|
+
`[brain] Summary: ${summaryPath}`,
|
|
274
|
+
'',
|
|
275
|
+
'IMPORTANT: Use the Agent tool (subagent_type: "brain-executor") to spawn the executor.',
|
|
276
|
+
'Do NOT execute the tasks yourself.',
|
|
277
|
+
'',
|
|
278
|
+
`After executor completes, run: ${nextCmd}`,
|
|
279
|
+
'',
|
|
280
|
+
prompt
|
|
281
|
+
].join('\n');
|
|
282
|
+
|
|
283
|
+
const result = {
|
|
284
|
+
action: 'spawn-quick-executor',
|
|
285
|
+
task: taskNum,
|
|
286
|
+
planPath,
|
|
287
|
+
summaryPath,
|
|
288
|
+
prompt,
|
|
289
|
+
model,
|
|
290
|
+
nextAction: nextCmd
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
output(result, humanText);
|
|
294
|
+
return result;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Step 6: Generate verifier instructions (--full mode only).
|
|
299
|
+
*/
|
|
300
|
+
function handleVerify(args, brainDir, state) {
|
|
301
|
+
const taskNum = getTaskNum(args);
|
|
302
|
+
if (!taskNum) {
|
|
303
|
+
error('--verify requires --task <number>');
|
|
304
|
+
return { error: 'missing-task-number' };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const taskDir = findTaskDir(brainDir, taskNum);
|
|
308
|
+
if (!taskDir) {
|
|
309
|
+
error(`Quick task ${taskNum} not found.`);
|
|
310
|
+
return { error: 'task-not-found' };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const planPath = path.join(taskDir, 'PLAN-1.md');
|
|
314
|
+
const verificationPath = path.join(taskDir, 'VERIFICATION-1.md');
|
|
315
|
+
|
|
316
|
+
// Load verifier template
|
|
317
|
+
const template = loadTemplate('verifier');
|
|
318
|
+
|
|
319
|
+
// Extract must_haves from plan
|
|
320
|
+
let mustHaves = 'No must_haves found in plan.';
|
|
321
|
+
try {
|
|
322
|
+
const planContent = fs.readFileSync(planPath, 'utf8');
|
|
323
|
+
const truthsMatch = planContent.match(/truths:\s*\n((?:\s+-\s+"[^"]*"\n?)*)/);
|
|
324
|
+
if (truthsMatch) {
|
|
325
|
+
mustHaves = truthsMatch[0];
|
|
326
|
+
}
|
|
327
|
+
} catch { /* ignore */ }
|
|
328
|
+
|
|
329
|
+
const prompt = interpolate(template, {
|
|
330
|
+
must_haves: mustHaves,
|
|
331
|
+
output_path: verificationPath,
|
|
332
|
+
anti_pattern_results: 'No anti-pattern scan for quick tasks.',
|
|
333
|
+
nyquist_section: 'Nyquist validation not applicable for quick tasks.'
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
const model = resolveModel('verifier', state);
|
|
337
|
+
|
|
338
|
+
logEvent(brainDir, 0, { type: 'quick-verify', task: taskNum });
|
|
339
|
+
|
|
340
|
+
const humanText = [
|
|
341
|
+
`[brain] Verifying Quick Task ${taskNum}`,
|
|
342
|
+
'',
|
|
343
|
+
'IMPORTANT: Use the Agent tool (subagent_type: "brain-verifier") to spawn the verifier.',
|
|
344
|
+
'',
|
|
345
|
+
`After verification, run: npx brain-dev quick --complete --task ${taskNum}`,
|
|
346
|
+
'',
|
|
347
|
+
prompt
|
|
348
|
+
].join('\n');
|
|
349
|
+
|
|
350
|
+
const result = {
|
|
351
|
+
action: 'spawn-quick-verifier',
|
|
352
|
+
task: taskNum,
|
|
353
|
+
prompt,
|
|
354
|
+
model,
|
|
355
|
+
verificationPath,
|
|
356
|
+
nextAction: `npx brain-dev quick --complete --task ${taskNum}`
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
output(result, humanText);
|
|
360
|
+
return result;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Step 7-8: Update STATE.md and output commit instructions.
|
|
365
|
+
*/
|
|
366
|
+
function handleComplete(args, brainDir, state) {
|
|
367
|
+
const taskNum = getTaskNum(args);
|
|
368
|
+
if (!taskNum) {
|
|
369
|
+
error('--complete requires --task <number>');
|
|
370
|
+
return { error: 'missing-task-number' };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const taskDir = findTaskDir(brainDir, taskNum);
|
|
374
|
+
if (!taskDir) {
|
|
375
|
+
error(`Quick task ${taskNum} not found.`);
|
|
376
|
+
return { error: 'task-not-found' };
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Read task metadata
|
|
380
|
+
let meta = { description: 'Unknown task', full: false };
|
|
381
|
+
try {
|
|
382
|
+
meta = JSON.parse(fs.readFileSync(path.join(taskDir, 'task.json'), 'utf8'));
|
|
383
|
+
} catch { /* ignore */ }
|
|
384
|
+
|
|
385
|
+
const date = new Date().toISOString().split('T')[0];
|
|
386
|
+
const slug = path.basename(taskDir).replace(/^\d+-/, '');
|
|
387
|
+
|
|
388
|
+
// Determine status
|
|
389
|
+
let status = 'Done';
|
|
390
|
+
if (meta.full) {
|
|
391
|
+
const verPath = path.join(taskDir, 'VERIFICATION-1.md');
|
|
392
|
+
if (fs.existsSync(verPath)) {
|
|
393
|
+
const content = fs.readFileSync(verPath, 'utf8');
|
|
394
|
+
if (content.includes('status: passed')) status = 'Verified';
|
|
395
|
+
else if (content.includes('status: human_needed')) status = 'Needs Review';
|
|
396
|
+
else if (content.includes('status: gaps_found')) status = 'Gaps';
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Update quick task count in state
|
|
401
|
+
if (!state.quick) state.quick = { count: 0 };
|
|
402
|
+
state.quick.count = taskNum;
|
|
403
|
+
writeState(brainDir, state);
|
|
404
|
+
|
|
405
|
+
logEvent(brainDir, 0, { type: 'quick-complete', task: taskNum, status });
|
|
406
|
+
|
|
407
|
+
// Build commit file list
|
|
408
|
+
const filesToCommit = [];
|
|
409
|
+
for (const f of ['PLAN-1.md', 'SUMMARY-1.md', 'VERIFICATION-1.md', 'task.json']) {
|
|
410
|
+
if (fs.existsSync(path.join(taskDir, f))) {
|
|
411
|
+
filesToCommit.push(path.relative(path.dirname(brainDir), path.join(taskDir, f)));
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
filesToCommit.push('.brain/brain.json');
|
|
415
|
+
filesToCommit.push('.brain/STATE.md');
|
|
416
|
+
|
|
417
|
+
const humanText = [
|
|
418
|
+
`[brain] Quick Task ${taskNum} Complete!`,
|
|
419
|
+
'',
|
|
420
|
+
`Task: ${meta.description}`,
|
|
421
|
+
`Status: ${status}`,
|
|
422
|
+
`Directory: ${taskDir}`,
|
|
423
|
+
'',
|
|
424
|
+
'Update STATE.md with a "Quick Tasks" section if not present, add this row:',
|
|
425
|
+
`| ${taskNum} | ${meta.description} | ${date} | ${status} | [${taskNum}-${slug}](./quick/${taskNum}-${slug}/) |`,
|
|
426
|
+
'',
|
|
427
|
+
`Then commit: git add ${filesToCommit.join(' ')} && git commit -m "quick(${taskNum}): ${meta.description}"`,
|
|
428
|
+
'',
|
|
429
|
+
'Ready for next: /brain:quick or /brain:progress'
|
|
430
|
+
].join('\n');
|
|
431
|
+
|
|
432
|
+
const result = {
|
|
433
|
+
action: 'quick-complete',
|
|
434
|
+
task: taskNum,
|
|
435
|
+
description: meta.description,
|
|
436
|
+
status,
|
|
437
|
+
date,
|
|
438
|
+
filesToCommit,
|
|
439
|
+
commitMessage: `quick(${taskNum}): ${meta.description}`,
|
|
440
|
+
nextAction: '/brain:quick'
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
output(result, humanText);
|
|
444
|
+
return result;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
module.exports = { run, generateSlug, calculateNextNum, findTaskDir };
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { readState } = require('../state.cjs');
|
|
6
|
+
const { output, prefix, error } = require('../core.cjs');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Parse simple YAML frontmatter from markdown content.
|
|
10
|
+
* @param {string} content - Markdown with YAML frontmatter
|
|
11
|
+
* @returns {{ frontmatter: object, body: string }}
|
|
12
|
+
*/
|
|
13
|
+
function parseFrontmatter(content) {
|
|
14
|
+
const fm = { frontmatter: {}, body: content };
|
|
15
|
+
|
|
16
|
+
if (!content.startsWith('---')) return fm;
|
|
17
|
+
|
|
18
|
+
const endIdx = content.indexOf('---', 3);
|
|
19
|
+
if (endIdx === -1) return fm;
|
|
20
|
+
|
|
21
|
+
const yamlBlock = content.slice(3, endIdx).trim();
|
|
22
|
+
const body = content.slice(endIdx + 3).trim();
|
|
23
|
+
|
|
24
|
+
const frontmatter = {};
|
|
25
|
+
for (const line of yamlBlock.split('\n')) {
|
|
26
|
+
const colonIdx = line.indexOf(':');
|
|
27
|
+
if (colonIdx === -1) continue;
|
|
28
|
+
const key = line.slice(0, colonIdx).trim();
|
|
29
|
+
let value = line.slice(colonIdx + 1).trim();
|
|
30
|
+
// Try to parse numbers
|
|
31
|
+
if (/^\d+$/.test(value)) value = parseInt(value, 10);
|
|
32
|
+
frontmatter[key] = value;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
fm.frontmatter = frontmatter;
|
|
36
|
+
fm.body = body;
|
|
37
|
+
return fm;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Extract named sections from markdown body.
|
|
42
|
+
* Sections start with ## heading.
|
|
43
|
+
* @param {string} body - Markdown body (after frontmatter)
|
|
44
|
+
* @returns {object} Map of camelCase section names to content
|
|
45
|
+
*/
|
|
46
|
+
function parseSections(body) {
|
|
47
|
+
const sections = {};
|
|
48
|
+
const lines = body.split('\n');
|
|
49
|
+
let currentKey = null;
|
|
50
|
+
let currentLines = [];
|
|
51
|
+
|
|
52
|
+
for (const line of lines) {
|
|
53
|
+
if (line.startsWith('## ')) {
|
|
54
|
+
// Save previous section
|
|
55
|
+
if (currentKey) {
|
|
56
|
+
sections[currentKey] = currentLines.join('\n').trim();
|
|
57
|
+
}
|
|
58
|
+
// New section
|
|
59
|
+
const heading = line.slice(3).trim();
|
|
60
|
+
currentKey = heading
|
|
61
|
+
.replace(/[^a-zA-Z0-9\s]/g, '')
|
|
62
|
+
.split(/\s+/)
|
|
63
|
+
.map((w, i) => i === 0 ? w.toLowerCase() : w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
|
|
64
|
+
.join('');
|
|
65
|
+
currentLines = [];
|
|
66
|
+
} else if (currentKey) {
|
|
67
|
+
currentLines.push(line);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Save last section
|
|
72
|
+
if (currentKey) {
|
|
73
|
+
sections[currentKey] = currentLines.join('\n').trim();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return sections;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Run the resume command.
|
|
81
|
+
* @param {string[]} args - CLI arguments
|
|
82
|
+
* @param {object} [opts] - Options (brainDir for testing)
|
|
83
|
+
* @returns {object} Result
|
|
84
|
+
*/
|
|
85
|
+
async function run(args = [], opts = {}) {
|
|
86
|
+
const brainDir = opts.brainDir || path.join(process.cwd(), '.brain');
|
|
87
|
+
const state = readState(brainDir);
|
|
88
|
+
|
|
89
|
+
// Check --session flag
|
|
90
|
+
const sessionIdx = args.indexOf('--session');
|
|
91
|
+
const sessionId = sessionIdx !== -1 && sessionIdx + 1 < args.length ? args[sessionIdx + 1] : null;
|
|
92
|
+
|
|
93
|
+
let snapshotContent = null;
|
|
94
|
+
let snapshotSource = null;
|
|
95
|
+
|
|
96
|
+
if (sessionId) {
|
|
97
|
+
// Load specific session
|
|
98
|
+
const sessionPath = path.join(brainDir, 'sessions', `${sessionId}.md`);
|
|
99
|
+
if (fs.existsSync(sessionPath)) {
|
|
100
|
+
snapshotContent = fs.readFileSync(sessionPath, 'utf8');
|
|
101
|
+
snapshotSource = sessionPath;
|
|
102
|
+
} else {
|
|
103
|
+
error(`Session not found: ${sessionId}`);
|
|
104
|
+
return { error: `Session not found: ${sessionId}` };
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
// Check for continue-here.md
|
|
108
|
+
const defaultPath = path.join(brainDir, 'continue-here.md');
|
|
109
|
+
if (fs.existsSync(defaultPath) && fs.statSync(defaultPath).isFile()) {
|
|
110
|
+
snapshotContent = fs.readFileSync(defaultPath, 'utf8');
|
|
111
|
+
snapshotSource = defaultPath;
|
|
112
|
+
} else {
|
|
113
|
+
// Check if sessions directory has files
|
|
114
|
+
const sessionsDir = path.join(brainDir, 'sessions');
|
|
115
|
+
if (fs.existsSync(sessionsDir)) {
|
|
116
|
+
const sessionFiles = fs.readdirSync(sessionsDir)
|
|
117
|
+
.filter(f => f.endsWith('.md'))
|
|
118
|
+
.sort();
|
|
119
|
+
|
|
120
|
+
if (sessionFiles.length > 0) {
|
|
121
|
+
// List available sessions
|
|
122
|
+
const sessions = sessionFiles.map(f => {
|
|
123
|
+
const content = fs.readFileSync(path.join(sessionsDir, f), 'utf8');
|
|
124
|
+
const { body } = parseFrontmatter(content);
|
|
125
|
+
const sections = parseSections(body);
|
|
126
|
+
const firstLine = (sections.currentState || '').split('\n')[0] || '';
|
|
127
|
+
return {
|
|
128
|
+
id: f.replace('.md', ''),
|
|
129
|
+
file: f,
|
|
130
|
+
summary: firstLine
|
|
131
|
+
};
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
output(
|
|
135
|
+
{ sessions },
|
|
136
|
+
[
|
|
137
|
+
prefix('Multiple sessions available. Use --session <id> to select:'),
|
|
138
|
+
...sessions.map(s => ` ${s.id} ${s.summary}`)
|
|
139
|
+
].join('\n')
|
|
140
|
+
);
|
|
141
|
+
return { sessions };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
error('No paused session found. Use /brain:pause to save a session first.');
|
|
146
|
+
return { error: 'No paused session found' };
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Parse snapshot
|
|
151
|
+
const { frontmatter, body } = parseFrontmatter(snapshotContent);
|
|
152
|
+
const sections = parseSections(body);
|
|
153
|
+
|
|
154
|
+
// Build briefing
|
|
155
|
+
const briefingLines = [
|
|
156
|
+
prefix('=== Session Briefing ==='),
|
|
157
|
+
'',
|
|
158
|
+
prefix(`Phase: ${frontmatter.phase || '?'} (${frontmatter.status || '?'})`),
|
|
159
|
+
prefix(`Paused at: ${frontmatter.paused_at || 'unknown'}`),
|
|
160
|
+
''
|
|
161
|
+
];
|
|
162
|
+
|
|
163
|
+
if (sections.currentState) {
|
|
164
|
+
briefingLines.push(prefix('Current State:'));
|
|
165
|
+
briefingLines.push(` ${sections.currentState}`);
|
|
166
|
+
briefingLines.push('');
|
|
167
|
+
}
|
|
168
|
+
if (sections.completedWork) {
|
|
169
|
+
briefingLines.push(prefix('Completed:'));
|
|
170
|
+
for (const line of sections.completedWork.split('\n').filter(l => l.trim())) {
|
|
171
|
+
briefingLines.push(` ${line}`);
|
|
172
|
+
}
|
|
173
|
+
briefingLines.push('');
|
|
174
|
+
}
|
|
175
|
+
if (sections.remainingWork) {
|
|
176
|
+
briefingLines.push(prefix('Remaining:'));
|
|
177
|
+
for (const line of sections.remainingWork.split('\n').filter(l => l.trim())) {
|
|
178
|
+
briefingLines.push(` ${line}`);
|
|
179
|
+
}
|
|
180
|
+
briefingLines.push('');
|
|
181
|
+
}
|
|
182
|
+
if (sections.nextAction) {
|
|
183
|
+
briefingLines.push(prefix(`Next: ${sections.nextAction}`));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const result = {
|
|
187
|
+
briefing: true,
|
|
188
|
+
snapshot: { frontmatter, sections },
|
|
189
|
+
source: snapshotSource
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
output(result, briefingLines.join('\n'));
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
module.exports = { run };
|