brain-dev 0.2.0 → 1.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/README.md +85 -23
- package/bin/brain-tools.cjs +8 -0
- package/bin/lib/commands/new-project.cjs +72 -782
- package/bin/lib/commands/new-task.cjs +522 -0
- package/bin/lib/commands/progress.cjs +1 -1
- package/bin/lib/commands/story.cjs +972 -0
- package/bin/lib/commands.cjs +21 -3
- package/bin/lib/state.cjs +47 -1
- package/bin/lib/story-helpers.cjs +439 -0
- package/commands/brain/new-task.md +31 -0
- package/commands/brain/story.md +35 -0
- package/hooks/bootstrap.sh +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { parseArgs } = require('node:util');
|
|
6
|
+
const { readState, writeState } = require('../state.cjs');
|
|
7
|
+
const { logEvent } = require('../logger.cjs');
|
|
8
|
+
const { output, error, prefix } = require('../core.cjs');
|
|
9
|
+
const { recordInvocation, estimateTokens } = require('../cost.cjs');
|
|
10
|
+
|
|
11
|
+
async function run(args = [], opts = {}) {
|
|
12
|
+
const { values, positionals } = parseArgs({
|
|
13
|
+
args,
|
|
14
|
+
options: {
|
|
15
|
+
research: { type: 'boolean', default: false },
|
|
16
|
+
light: { type: 'boolean', default: false },
|
|
17
|
+
continue: { type: 'boolean', default: false },
|
|
18
|
+
list: { type: 'boolean', default: false },
|
|
19
|
+
promote: { type: 'string' },
|
|
20
|
+
json: { type: 'boolean', default: false }
|
|
21
|
+
},
|
|
22
|
+
strict: false,
|
|
23
|
+
allowPositionals: true
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const brainDir = opts.brainDir || path.join(process.cwd(), '.brain');
|
|
27
|
+
const state = readState(brainDir);
|
|
28
|
+
|
|
29
|
+
if (!state || !state.project?.initialized) {
|
|
30
|
+
error("Project not initialized. Run '/brain:new-project' first.");
|
|
31
|
+
return { error: 'not-initialized' };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Handle --list
|
|
35
|
+
if (values.list) return handleList(brainDir, state);
|
|
36
|
+
|
|
37
|
+
// Handle --promote N
|
|
38
|
+
if (values.promote) {
|
|
39
|
+
const taskNum = parseInt(values.promote, 10);
|
|
40
|
+
if (isNaN(taskNum) || taskNum < 1) {
|
|
41
|
+
error("--promote requires a valid task number. Usage: brain-dev new-task --promote <N>");
|
|
42
|
+
return { error: 'invalid-task-number' };
|
|
43
|
+
}
|
|
44
|
+
return handlePromote(brainDir, state, taskNum);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Handle --continue (resume current task)
|
|
48
|
+
if (values.continue) return handleContinue(brainDir, state);
|
|
49
|
+
|
|
50
|
+
// New task creation
|
|
51
|
+
const description = positionals.join(' ').trim();
|
|
52
|
+
if (!description) {
|
|
53
|
+
error("Usage: brain-dev new-task \"description\" [--research] [--light]");
|
|
54
|
+
return { error: 'no-description' };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return handleNewTask(brainDir, state, description, values);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function handleNewTask(brainDir, state, description, options) {
|
|
61
|
+
// Create task directory and metadata
|
|
62
|
+
const taskNum = (state.tasks?.count || 0) + 1;
|
|
63
|
+
const slug = description
|
|
64
|
+
.toLowerCase()
|
|
65
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
66
|
+
.replace(/^-|-$/g, '')
|
|
67
|
+
.slice(0, 40) || 'task';
|
|
68
|
+
|
|
69
|
+
const taskDir = path.join(brainDir, 'tasks', `${String(taskNum).padStart(2, '0')}-${slug}`);
|
|
70
|
+
fs.mkdirSync(taskDir, { recursive: true });
|
|
71
|
+
|
|
72
|
+
const mode = options.light ? 'light' : 'standard';
|
|
73
|
+
const taskMeta = {
|
|
74
|
+
num: taskNum,
|
|
75
|
+
slug,
|
|
76
|
+
description,
|
|
77
|
+
created: new Date().toISOString(),
|
|
78
|
+
status: 'initialized',
|
|
79
|
+
mode,
|
|
80
|
+
research: options.research || false,
|
|
81
|
+
parent_phase: state.phase?.current || null,
|
|
82
|
+
promoted_to_phase: null,
|
|
83
|
+
completed: null
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
fs.writeFileSync(path.join(taskDir, 'task.json'), JSON.stringify(taskMeta, null, 2));
|
|
87
|
+
|
|
88
|
+
// Update state
|
|
89
|
+
state.tasks = state.tasks || { current: null, count: 0, active: [], history: [] };
|
|
90
|
+
state.tasks.current = taskNum;
|
|
91
|
+
state.tasks.count = taskNum;
|
|
92
|
+
state.tasks.active.push({ num: taskNum, slug, status: 'initialized', description });
|
|
93
|
+
writeState(brainDir, state);
|
|
94
|
+
|
|
95
|
+
logEvent(brainDir, 0, { type: 'task-init', task: taskNum, slug, description, mode });
|
|
96
|
+
|
|
97
|
+
// Determine first step
|
|
98
|
+
const firstStep = mode === 'light' ? 'plan' : 'discuss';
|
|
99
|
+
|
|
100
|
+
// Build step instructions
|
|
101
|
+
const steps = buildStepInstructions(taskNum, slug, taskDir, brainDir, mode, options.research, description);
|
|
102
|
+
|
|
103
|
+
const humanLines = [
|
|
104
|
+
prefix(`Task #${taskNum} created: ${description}`),
|
|
105
|
+
prefix(`Mode: ${mode}${options.research ? ' + research' : ''}`),
|
|
106
|
+
prefix(`Directory: .brain/tasks/${String(taskNum).padStart(2, '0')}-${slug}/`),
|
|
107
|
+
'',
|
|
108
|
+
prefix(`Pipeline: ${mode === 'light' ? 'plan -> execute -> commit' : 'discuss -> plan -> execute -> verify -> complete'}`),
|
|
109
|
+
'',
|
|
110
|
+
prefix(`Next step: ${firstStep}`),
|
|
111
|
+
'',
|
|
112
|
+
steps[firstStep]
|
|
113
|
+
].join('\n');
|
|
114
|
+
|
|
115
|
+
const result = {
|
|
116
|
+
action: 'task-created',
|
|
117
|
+
task: taskMeta,
|
|
118
|
+
nextStep: firstStep,
|
|
119
|
+
taskDir,
|
|
120
|
+
steps,
|
|
121
|
+
instructions: steps[firstStep]
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
output(result, humanLines);
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function buildStepInstructions(taskNum, slug, taskDir, brainDir, mode, research, description) {
|
|
129
|
+
const padded = String(taskNum).padStart(2, '0');
|
|
130
|
+
const steps = {};
|
|
131
|
+
|
|
132
|
+
// DISCUSS step
|
|
133
|
+
steps.discuss = [
|
|
134
|
+
`## Step 1: Discuss — Task #${taskNum}`,
|
|
135
|
+
'',
|
|
136
|
+
`Discuss the architectural approach for: "${description}"`,
|
|
137
|
+
'',
|
|
138
|
+
'Questions to explore:',
|
|
139
|
+
'1. What is the best approach for this task?',
|
|
140
|
+
'2. Are there existing patterns in the codebase to follow?',
|
|
141
|
+
'3. What are the risks or edge cases?',
|
|
142
|
+
'4. What dependencies does this have?',
|
|
143
|
+
'',
|
|
144
|
+
'After discussion, save decisions to:',
|
|
145
|
+
`\`${taskDir}/CONTEXT.md\``,
|
|
146
|
+
'',
|
|
147
|
+
'Then run: `npx brain-dev new-task --continue`'
|
|
148
|
+
].join('\n');
|
|
149
|
+
|
|
150
|
+
// RESEARCH step (optional)
|
|
151
|
+
if (research) {
|
|
152
|
+
steps.research = [
|
|
153
|
+
`## Step 1.5: Research — Task #${taskNum}`,
|
|
154
|
+
'',
|
|
155
|
+
`Research the approach for: "${description}"`,
|
|
156
|
+
'',
|
|
157
|
+
'Focus on:',
|
|
158
|
+
'1. How does the existing codebase handle similar features?',
|
|
159
|
+
'2. What are the best practices for this type of change?',
|
|
160
|
+
'',
|
|
161
|
+
'Save findings to:',
|
|
162
|
+
`\`${taskDir}/RESEARCH.md\``,
|
|
163
|
+
'',
|
|
164
|
+
'Then run: `npx brain-dev new-task --continue`'
|
|
165
|
+
].join('\n');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// PLAN step
|
|
169
|
+
steps.plan = [
|
|
170
|
+
`## Step 2: Plan — Task #${taskNum}`,
|
|
171
|
+
'',
|
|
172
|
+
`Create an execution plan for: "${description}"`,
|
|
173
|
+
'',
|
|
174
|
+
'Constraints:',
|
|
175
|
+
'- Max 3-4 tasks (this is a focused task, not a full phase)',
|
|
176
|
+
'- Target ~50% context budget',
|
|
177
|
+
'- Each task should be atomic and testable',
|
|
178
|
+
'- Include must_haves for verification',
|
|
179
|
+
'',
|
|
180
|
+
`Read context: \`${taskDir}/CONTEXT.md\` (if exists)`,
|
|
181
|
+
research ? `Read research: \`${taskDir}/RESEARCH.md\` (if exists)` : '',
|
|
182
|
+
'',
|
|
183
|
+
`Write plan to: \`${taskDir}/PLAN-1.md\``,
|
|
184
|
+
'Use standard Brain plan format with YAML frontmatter.',
|
|
185
|
+
'',
|
|
186
|
+
'Then run: `npx brain-dev new-task --continue`'
|
|
187
|
+
].filter(Boolean).join('\n');
|
|
188
|
+
|
|
189
|
+
// EXECUTE step
|
|
190
|
+
steps.execute = [
|
|
191
|
+
`## Step 3: Execute — Task #${taskNum}`,
|
|
192
|
+
'',
|
|
193
|
+
`Execute the plan at: \`${taskDir}/PLAN-1.md\``,
|
|
194
|
+
'',
|
|
195
|
+
'Instructions:',
|
|
196
|
+
'- Follow the plan tasks sequentially',
|
|
197
|
+
'- Make atomic commits per task',
|
|
198
|
+
'- If a task fails, retry once then escalate',
|
|
199
|
+
'',
|
|
200
|
+
`Write summary to: \`${taskDir}/SUMMARY-1.md\``,
|
|
201
|
+
'Include: key files modified, decisions made, Self-Check status.',
|
|
202
|
+
'',
|
|
203
|
+
'Then run: `npx brain-dev new-task --continue`'
|
|
204
|
+
].join('\n');
|
|
205
|
+
|
|
206
|
+
// VERIFY step
|
|
207
|
+
if (mode !== 'light') {
|
|
208
|
+
steps.verify = [
|
|
209
|
+
`## Step 4: Verify — Task #${taskNum}`,
|
|
210
|
+
'',
|
|
211
|
+
`Verify the execution results against must_haves in: \`${taskDir}/PLAN-1.md\``,
|
|
212
|
+
'',
|
|
213
|
+
'Verification levels:',
|
|
214
|
+
'1. EXISTS: Files exist and are non-empty',
|
|
215
|
+
'2. SUBSTANTIVE: Contains real implementation (no stubs)',
|
|
216
|
+
'3. WIRED: Properly imported and consumed',
|
|
217
|
+
'',
|
|
218
|
+
`Write results to: \`${taskDir}/VERIFICATION.md\``,
|
|
219
|
+
'',
|
|
220
|
+
'Then run: `npx brain-dev new-task --continue`'
|
|
221
|
+
].join('\n');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// COMPLETE step
|
|
225
|
+
steps.complete = [
|
|
226
|
+
`## Step 5: Complete — Task #${taskNum}`,
|
|
227
|
+
'',
|
|
228
|
+
'Task completion options:',
|
|
229
|
+
'',
|
|
230
|
+
'**Option A: Keep as standalone task**',
|
|
231
|
+
'`npx brain-dev new-task --continue` (will auto-complete)',
|
|
232
|
+
'',
|
|
233
|
+
'**Option B: Promote to roadmap phase**',
|
|
234
|
+
`\`npx brain-dev new-task --promote ${taskNum}\``,
|
|
235
|
+
'This adds the task as a new phase in ROADMAP.md.',
|
|
236
|
+
'',
|
|
237
|
+
'Commit suggestion:',
|
|
238
|
+
`\`git add -A && git commit -m "feat(task-${padded}): ${slug}"\``
|
|
239
|
+
].join('\n');
|
|
240
|
+
|
|
241
|
+
return steps;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function handleContinue(brainDir, state) {
|
|
245
|
+
const taskNum = state.tasks?.current;
|
|
246
|
+
if (!taskNum) {
|
|
247
|
+
error("No active task. Start one with: brain-dev new-task \"description\"");
|
|
248
|
+
return { error: 'no-active-task' };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Find task directory
|
|
252
|
+
const tasksDir = path.join(brainDir, 'tasks');
|
|
253
|
+
if (!fs.existsSync(tasksDir)) {
|
|
254
|
+
error("No tasks directory found.");
|
|
255
|
+
return { error: 'no-tasks-dir' };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const padded = String(taskNum).padStart(2, '0');
|
|
259
|
+
const dirs = fs.readdirSync(tasksDir).filter(d => d.startsWith(padded + '-'));
|
|
260
|
+
if (dirs.length === 0) {
|
|
261
|
+
error(`Task #${taskNum} directory not found.`);
|
|
262
|
+
return { error: 'task-not-found' };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const taskDir = path.join(tasksDir, dirs[0]);
|
|
266
|
+
const taskMeta = JSON.parse(fs.readFileSync(path.join(taskDir, 'task.json'), 'utf8'));
|
|
267
|
+
|
|
268
|
+
// Determine current step based on what files exist
|
|
269
|
+
const hasContext = fs.existsSync(path.join(taskDir, 'CONTEXT.md'));
|
|
270
|
+
const hasResearch = fs.existsSync(path.join(taskDir, 'RESEARCH.md'));
|
|
271
|
+
const hasPlan = fs.existsSync(path.join(taskDir, 'PLAN-1.md'));
|
|
272
|
+
const hasSummary = fs.existsSync(path.join(taskDir, 'SUMMARY-1.md'));
|
|
273
|
+
const hasVerification = fs.existsSync(path.join(taskDir, 'VERIFICATION.md'));
|
|
274
|
+
|
|
275
|
+
const isLight = taskMeta.mode === 'light';
|
|
276
|
+
let nextStep;
|
|
277
|
+
let newStatus;
|
|
278
|
+
|
|
279
|
+
if (!hasContext && !isLight) {
|
|
280
|
+
nextStep = 'discuss';
|
|
281
|
+
newStatus = 'discussing';
|
|
282
|
+
} else if (taskMeta.research && !hasResearch) {
|
|
283
|
+
nextStep = 'research';
|
|
284
|
+
newStatus = 'researching';
|
|
285
|
+
} else if (!hasPlan) {
|
|
286
|
+
nextStep = 'plan';
|
|
287
|
+
newStatus = 'planning';
|
|
288
|
+
} else if (!hasSummary) {
|
|
289
|
+
nextStep = 'execute';
|
|
290
|
+
newStatus = 'executing';
|
|
291
|
+
} else if (!hasVerification && !isLight) {
|
|
292
|
+
nextStep = 'verify';
|
|
293
|
+
newStatus = 'verifying';
|
|
294
|
+
} else {
|
|
295
|
+
// All steps done — complete the task
|
|
296
|
+
return handleTaskComplete(brainDir, state, taskNum, taskDir, taskMeta);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Update status
|
|
300
|
+
taskMeta.status = newStatus;
|
|
301
|
+
fs.writeFileSync(path.join(taskDir, 'task.json'), JSON.stringify(taskMeta, null, 2));
|
|
302
|
+
|
|
303
|
+
// Update state
|
|
304
|
+
state.tasks.active = state.tasks.active || [];
|
|
305
|
+
const activeIdx = state.tasks.active.findIndex(t => t.num === taskNum);
|
|
306
|
+
if (activeIdx >= 0) state.tasks.active[activeIdx].status = newStatus;
|
|
307
|
+
writeState(brainDir, state);
|
|
308
|
+
|
|
309
|
+
// Build step instructions
|
|
310
|
+
const steps = buildStepInstructions(
|
|
311
|
+
taskNum, taskMeta.slug, taskDir, brainDir,
|
|
312
|
+
taskMeta.mode, taskMeta.research, taskMeta.description
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
logEvent(brainDir, 0, { type: 'task-continue', task: taskNum, step: nextStep });
|
|
316
|
+
|
|
317
|
+
// Record cost for execute step
|
|
318
|
+
if (nextStep === 'execute' && hasPlan) {
|
|
319
|
+
try {
|
|
320
|
+
const planContent = fs.readFileSync(path.join(taskDir, 'PLAN-1.md'), 'utf8');
|
|
321
|
+
const inputTokens = estimateTokens(planContent);
|
|
322
|
+
recordInvocation(brainDir, {
|
|
323
|
+
agent: 'executor', phase: 0, plan: '01',
|
|
324
|
+
model: 'inherit', input_tokens: inputTokens, output_tokens: 0
|
|
325
|
+
});
|
|
326
|
+
} catch { /* non-fatal */ }
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const humanLines = [
|
|
330
|
+
prefix(`Task #${taskNum}: ${taskMeta.description}`),
|
|
331
|
+
prefix(`Status: ${newStatus}`),
|
|
332
|
+
prefix(`Next step: ${nextStep}`),
|
|
333
|
+
'',
|
|
334
|
+
steps[nextStep]
|
|
335
|
+
].join('\n');
|
|
336
|
+
|
|
337
|
+
output({
|
|
338
|
+
action: 'task-continue',
|
|
339
|
+
task: taskMeta,
|
|
340
|
+
nextStep,
|
|
341
|
+
instructions: steps[nextStep]
|
|
342
|
+
}, humanLines);
|
|
343
|
+
|
|
344
|
+
return { action: 'task-continue', task: taskMeta, nextStep };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function handleTaskComplete(brainDir, state, taskNum, taskDir, taskMeta) {
|
|
348
|
+
taskMeta.status = 'complete';
|
|
349
|
+
taskMeta.completed = new Date().toISOString();
|
|
350
|
+
fs.writeFileSync(path.join(taskDir, 'task.json'), JSON.stringify(taskMeta, null, 2));
|
|
351
|
+
|
|
352
|
+
// Move from active to history
|
|
353
|
+
state.tasks.active = state.tasks.active.filter(t => t.num !== taskNum);
|
|
354
|
+
state.tasks.history.push({
|
|
355
|
+
num: taskNum,
|
|
356
|
+
slug: taskMeta.slug,
|
|
357
|
+
description: taskMeta.description,
|
|
358
|
+
mode: taskMeta.mode,
|
|
359
|
+
completed: taskMeta.completed,
|
|
360
|
+
promoted_to_phase: null
|
|
361
|
+
});
|
|
362
|
+
state.tasks.current = null;
|
|
363
|
+
writeState(brainDir, state);
|
|
364
|
+
|
|
365
|
+
logEvent(brainDir, 0, { type: 'task-complete', task: taskNum, slug: taskMeta.slug });
|
|
366
|
+
|
|
367
|
+
const padded = String(taskNum).padStart(2, '0');
|
|
368
|
+
const humanLines = [
|
|
369
|
+
prefix(`Task #${taskNum} complete: ${taskMeta.description}`),
|
|
370
|
+
'',
|
|
371
|
+
prefix('Options:'),
|
|
372
|
+
prefix(' 1. Keep as standalone task (done!)'),
|
|
373
|
+
prefix(` 2. Promote to roadmap phase: brain-dev new-task --promote ${taskNum}`),
|
|
374
|
+
'',
|
|
375
|
+
prefix(`Suggested commit: git add -A && git commit -m "feat(task-${padded}): ${taskMeta.slug}"`)
|
|
376
|
+
].join('\n');
|
|
377
|
+
|
|
378
|
+
output({
|
|
379
|
+
action: 'task-complete',
|
|
380
|
+
task: taskMeta,
|
|
381
|
+
promote_command: `brain-dev new-task --promote ${taskNum}`
|
|
382
|
+
}, humanLines);
|
|
383
|
+
|
|
384
|
+
return { action: 'task-complete', task: taskMeta };
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function handlePromote(brainDir, state, taskNum) {
|
|
388
|
+
// Guard: phases must exist for promotion
|
|
389
|
+
if (!state.phase || !Array.isArray(state.phase.phases)) {
|
|
390
|
+
error("Project phases not initialized. Run '/brain:new-project' first.");
|
|
391
|
+
return { error: 'no-phases' };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Find the task
|
|
395
|
+
const tasksDir = path.join(brainDir, 'tasks');
|
|
396
|
+
if (!fs.existsSync(tasksDir)) {
|
|
397
|
+
error(`No tasks directory found. Create a task first: brain-dev new-task "description"`);
|
|
398
|
+
return { error: 'no-tasks-dir' };
|
|
399
|
+
}
|
|
400
|
+
const padded = String(taskNum).padStart(2, '0');
|
|
401
|
+
const dirs = fs.readdirSync(tasksDir).filter(d => d.startsWith(padded + '-'));
|
|
402
|
+
|
|
403
|
+
if (dirs.length === 0) {
|
|
404
|
+
error(`Task #${taskNum} not found.`);
|
|
405
|
+
return { error: 'task-not-found' };
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const taskDir = path.join(tasksDir, dirs[0]);
|
|
409
|
+
const taskMeta = JSON.parse(fs.readFileSync(path.join(taskDir, 'task.json'), 'utf8'));
|
|
410
|
+
|
|
411
|
+
// Insert as a new phase after current phase
|
|
412
|
+
try {
|
|
413
|
+
const { parseRoadmap, insertPhase, writeRoadmap } = require('../roadmap.cjs');
|
|
414
|
+
const roadmapPath = path.join(brainDir, 'ROADMAP.md');
|
|
415
|
+
|
|
416
|
+
if (!fs.existsSync(roadmapPath)) {
|
|
417
|
+
error("No ROADMAP.md found. Cannot promote without a roadmap.");
|
|
418
|
+
return { error: 'no-roadmap' };
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const roadmapData = parseRoadmap(brainDir);
|
|
422
|
+
const afterPhase = state.phase?.current || roadmapData.phases.length;
|
|
423
|
+
|
|
424
|
+
const updatedRoadmap = insertPhase(roadmapData, afterPhase, {
|
|
425
|
+
name: taskMeta.description,
|
|
426
|
+
goal: `Implement: ${taskMeta.description} (promoted from task #${taskNum})`,
|
|
427
|
+
requirements: taskMeta.requirements || [],
|
|
428
|
+
status: taskMeta.status === 'complete' ? 'Complete' : 'In Progress'
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
writeRoadmap(brainDir, updatedRoadmap);
|
|
432
|
+
|
|
433
|
+
// Find the newly inserted phase (it was not in the original roadmapData)
|
|
434
|
+
const newPhase = updatedRoadmap.phases.find(
|
|
435
|
+
p => !roadmapData.phases.some(op => op.number === p.number)
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
// Update task metadata
|
|
439
|
+
taskMeta.promoted_to_phase = newPhase ? newPhase.number : afterPhase;
|
|
440
|
+
fs.writeFileSync(path.join(taskDir, 'task.json'), JSON.stringify(taskMeta, null, 2));
|
|
441
|
+
|
|
442
|
+
// Update state
|
|
443
|
+
state.phase.total = updatedRoadmap.phases.length;
|
|
444
|
+
state.phase.phases.push({
|
|
445
|
+
number: taskMeta.promoted_to_phase,
|
|
446
|
+
name: taskMeta.description,
|
|
447
|
+
status: taskMeta.status === 'complete' ? 'complete' : 'pending',
|
|
448
|
+
goal: newPhase ? newPhase.goal : taskMeta.description
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
// Update history
|
|
452
|
+
const historyEntry = state.tasks.history.find(t => t.num === taskNum);
|
|
453
|
+
if (historyEntry) historyEntry.promoted_to_phase = taskMeta.promoted_to_phase;
|
|
454
|
+
|
|
455
|
+
writeState(brainDir, state);
|
|
456
|
+
|
|
457
|
+
logEvent(brainDir, 0, { type: 'task-promote', task: taskNum, phase: taskMeta.promoted_to_phase });
|
|
458
|
+
|
|
459
|
+
// Copy artifacts to phase directory
|
|
460
|
+
const phaseSlug = taskMeta.slug;
|
|
461
|
+
const phasePadded = Number.isInteger(taskMeta.promoted_to_phase)
|
|
462
|
+
? String(taskMeta.promoted_to_phase).padStart(2, '0')
|
|
463
|
+
: String(taskMeta.promoted_to_phase).replace('.', '_');
|
|
464
|
+
const phaseDir = path.join(brainDir, 'phases', `${phasePadded}-${phaseSlug}`);
|
|
465
|
+
fs.mkdirSync(phaseDir, { recursive: true });
|
|
466
|
+
|
|
467
|
+
// Copy plan, summary, verification if they exist
|
|
468
|
+
for (const file of ['PLAN-1.md', 'SUMMARY-1.md', 'VERIFICATION.md', 'CONTEXT.md']) {
|
|
469
|
+
const src = path.join(taskDir, file);
|
|
470
|
+
if (fs.existsSync(src)) {
|
|
471
|
+
fs.copyFileSync(src, path.join(phaseDir, file));
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const humanLines = [
|
|
476
|
+
prefix(`Task #${taskNum} promoted to Phase ${taskMeta.promoted_to_phase}`),
|
|
477
|
+
prefix(`Phase: ${taskMeta.description}`),
|
|
478
|
+
prefix(`Artifacts copied to: .brain/phases/${phasePadded}-${phaseSlug}/`),
|
|
479
|
+
prefix('ROADMAP.md updated')
|
|
480
|
+
].join('\n');
|
|
481
|
+
|
|
482
|
+
output({ action: 'task-promoted', task: taskMeta, phase: newPhase }, humanLines);
|
|
483
|
+
return { action: 'task-promoted', task: taskMeta, phase: newPhase };
|
|
484
|
+
|
|
485
|
+
} catch (e) {
|
|
486
|
+
error(`Promotion failed: ${e.message}`);
|
|
487
|
+
return { error: 'promotion-failed', message: e.message };
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function handleList(brainDir, state) {
|
|
492
|
+
const tasks = state.tasks || { active: [], history: [], count: 0 };
|
|
493
|
+
|
|
494
|
+
const lines = [prefix(`Tasks (${tasks.count} total)`)];
|
|
495
|
+
|
|
496
|
+
if (tasks.active.length > 0) {
|
|
497
|
+
lines.push('');
|
|
498
|
+
lines.push(prefix('Active:'));
|
|
499
|
+
for (const t of tasks.active) {
|
|
500
|
+
lines.push(prefix(` #${t.num} ${t.slug} (${t.status})`));
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (tasks.history.length > 0) {
|
|
505
|
+
lines.push('');
|
|
506
|
+
lines.push(prefix('Completed:'));
|
|
507
|
+
for (const t of tasks.history.slice(-10)) {
|
|
508
|
+
const promoted = t.promoted_to_phase ? ` -> Phase ${t.promoted_to_phase}` : '';
|
|
509
|
+
lines.push(prefix(` #${t.num} ${t.slug}${promoted}`));
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (tasks.count === 0) {
|
|
514
|
+
lines.push(prefix(' No tasks yet. Create one: brain-dev new-task "description"'));
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const result = { action: 'task-list', tasks };
|
|
518
|
+
output(result, lines.join('\n'));
|
|
519
|
+
return result;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
module.exports = { run };
|