brain-dev 0.2.0 → 0.3.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 +30 -0
- package/bin/brain-tools.cjs +4 -0
- package/bin/lib/commands/new-task.cjs +522 -0
- package/bin/lib/commands.cjs +9 -0
- package/bin/lib/state.cjs +28 -1
- package/commands/brain/new-task.md +31 -0
- package/hooks/bootstrap.sh +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -106,6 +106,35 @@ Brain creates a plan, executes it, and commits — all in one flow. No discuss,
|
|
|
106
106
|
|
|
107
107
|
Quick tasks live in `.brain/quick/`, separate from phases. Tracked in STATE.md.
|
|
108
108
|
|
|
109
|
+
## Significant Tasks
|
|
110
|
+
|
|
111
|
+
For features that need more than a quick fix but less than a new project:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
/brain:new-task "add payment integration with Stripe"
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Brain runs the full pipeline: discuss → plan → execute → verify → complete.
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
/brain:new-task --research "add rate limiting" # With light research
|
|
121
|
+
/brain:new-task --light "fix auth redirect" # Skip discuss + verify
|
|
122
|
+
/brain:new-task --continue # Resume current task
|
|
123
|
+
/brain:new-task --list # List all tasks
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
After completion, promote to your roadmap:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
/brain:new-task --promote 1 # Adds task as a phase in ROADMAP.md
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
| Mode | Pipeline |
|
|
133
|
+
|------|----------|
|
|
134
|
+
| Standard | discuss → plan → execute → verify → complete |
|
|
135
|
+
| `--light` | plan → execute → commit |
|
|
136
|
+
| `--research` | discuss → research → plan → execute → verify → complete |
|
|
137
|
+
|
|
109
138
|
## State Machine
|
|
110
139
|
|
|
111
140
|
Brain tracks progress through a deterministic state machine:
|
|
@@ -146,6 +175,7 @@ No supply chain risk. No transitive vulnerabilities. No `node_modules` bloat.
|
|
|
146
175
|
| `/brain:verify` | 3-level verification against must-haves |
|
|
147
176
|
| `/brain:complete` | Mark phase done, advance to next |
|
|
148
177
|
| `/brain:quick` | Quick task — skip the ceremony |
|
|
178
|
+
| `/brain:new-task` | Significant task — full pipeline without a new project |
|
|
149
179
|
|
|
150
180
|
### Session
|
|
151
181
|
| Command | Description |
|
package/bin/brain-tools.cjs
CHANGED
|
@@ -132,6 +132,10 @@ async function main() {
|
|
|
132
132
|
await require('./lib/commands/quick.cjs').run(args.slice(1));
|
|
133
133
|
break;
|
|
134
134
|
|
|
135
|
+
case 'new-task':
|
|
136
|
+
await require('./lib/commands/new-task.cjs').run(args.slice(1));
|
|
137
|
+
break;
|
|
138
|
+
|
|
135
139
|
case 'execute':
|
|
136
140
|
if (args.includes('--auto') || args.includes('--dry-run') || args.includes('--stop')) {
|
|
137
141
|
await require('./lib/commands/auto.cjs').run(args.slice(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 };
|
package/bin/lib/commands.cjs
CHANGED
|
@@ -65,6 +65,15 @@ const COMMANDS = [
|
|
|
65
65
|
needsState: true,
|
|
66
66
|
args: ' --full Enable plan checking + verification\n --execute --task N Execute task N\n --verify --task N Verify task N (--full only)\n --complete --task N Complete and commit task N'
|
|
67
67
|
},
|
|
68
|
+
{
|
|
69
|
+
name: 'new-task',
|
|
70
|
+
description: 'Create a significant task with full pipeline',
|
|
71
|
+
usage: 'brain-dev new-task "description" [--research] [--light]',
|
|
72
|
+
group: 'Lifecycle',
|
|
73
|
+
implemented: true,
|
|
74
|
+
needsState: true,
|
|
75
|
+
args: ' --research Include light research phase\n --light Skip discuss and verify (fast mode)\n --continue Resume the current active task\n --list List all tasks\n --promote N Promote task N to roadmap phase'
|
|
76
|
+
},
|
|
68
77
|
{
|
|
69
78
|
name: 'execute',
|
|
70
79
|
description: 'Execute plans with verification',
|
package/bin/lib/state.cjs
CHANGED
|
@@ -4,7 +4,7 @@ const fs = require('node:fs');
|
|
|
4
4
|
const path = require('node:path');
|
|
5
5
|
|
|
6
6
|
const CURRENT_SCHEMA = 'brain/v1';
|
|
7
|
-
const CURRENT_VERSION = '0.
|
|
7
|
+
const CURRENT_VERSION = '0.9.0';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Atomic write: write to temp file, then rename.
|
|
@@ -176,6 +176,11 @@ function migrateState(data) {
|
|
|
176
176
|
migrated.phase.last_stuck_at = null;
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
+
// v0.9.0 fields (new-task)
|
|
180
|
+
if (!migrated.tasks) {
|
|
181
|
+
migrated.tasks = { current: null, count: 0, active: [], history: [] };
|
|
182
|
+
}
|
|
183
|
+
|
|
179
184
|
return migrated;
|
|
180
185
|
}
|
|
181
186
|
|
|
@@ -320,6 +325,22 @@ function generateStateMd(state) {
|
|
|
320
325
|
lines.push('');
|
|
321
326
|
}
|
|
322
327
|
|
|
328
|
+
// Active Tasks section (v0.9.0)
|
|
329
|
+
if (state.tasks && state.tasks.active && state.tasks.active.length > 0) {
|
|
330
|
+
lines.push('');
|
|
331
|
+
lines.push('## Active Tasks');
|
|
332
|
+
for (const t of state.tasks.active) {
|
|
333
|
+
lines.push(`- Task ${t.num}: ${t.slug} (${t.status})`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
if (state.tasks && state.tasks.count > 0) {
|
|
337
|
+
lines.push('');
|
|
338
|
+
lines.push(`## Task Stats`);
|
|
339
|
+
lines.push(`- Total: ${state.tasks.count}`);
|
|
340
|
+
lines.push(`- Active: ${state.tasks.active ? state.tasks.active.length : 0}`);
|
|
341
|
+
lines.push(`- Completed: ${state.tasks.history ? state.tasks.history.length : 0}`);
|
|
342
|
+
}
|
|
343
|
+
|
|
323
344
|
lines.push('## Blockers');
|
|
324
345
|
|
|
325
346
|
if (Array.isArray(blockers) && blockers.length > 0) {
|
|
@@ -441,6 +462,12 @@ function createDefaultState(platform) {
|
|
|
441
462
|
preinline: true,
|
|
442
463
|
budgets: {},
|
|
443
464
|
condensed_summaries: true
|
|
465
|
+
},
|
|
466
|
+
tasks: {
|
|
467
|
+
current: null,
|
|
468
|
+
count: 0,
|
|
469
|
+
active: [],
|
|
470
|
+
history: []
|
|
444
471
|
}
|
|
445
472
|
};
|
|
446
473
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: brain:new-task
|
|
3
|
+
description: Create a significant task with full discuss → plan → execute → verify → complete pipeline
|
|
4
|
+
allowed-tools:
|
|
5
|
+
- Read
|
|
6
|
+
- Write
|
|
7
|
+
- Edit
|
|
8
|
+
- Bash
|
|
9
|
+
- Grep
|
|
10
|
+
- Glob
|
|
11
|
+
- Agent
|
|
12
|
+
- AskUserQuestion
|
|
13
|
+
---
|
|
14
|
+
<objective>
|
|
15
|
+
Create and execute a significant task that needs thoughtful planning, execution, and verification.
|
|
16
|
+
Unlike quick tasks, new-task applies the full pipeline. Unlike new-project, it works within the existing project context.
|
|
17
|
+
</objective>
|
|
18
|
+
|
|
19
|
+
<context>
|
|
20
|
+
Use new-task for features, refactors, or epics that need more ceremony than a quick fix but less than a new project.
|
|
21
|
+
Examples: "add payment integration", "refactor auth to JWT", "add rate limiting to API"
|
|
22
|
+
</context>
|
|
23
|
+
|
|
24
|
+
<process>
|
|
25
|
+
1. Create: `npx brain-dev new-task "description"` [--research] [--light]
|
|
26
|
+
2. Follow the step instructions output by the command
|
|
27
|
+
3. After each step completes, run: `npx brain-dev new-task --continue`
|
|
28
|
+
4. Pipeline: discuss → plan → execute → verify → complete
|
|
29
|
+
5. On completion, optionally promote to roadmap: `npx brain-dev new-task --promote N`
|
|
30
|
+
6. List tasks: `npx brain-dev new-task --list`
|
|
31
|
+
</process>
|
package/hooks/bootstrap.sh
CHANGED
|
@@ -48,7 +48,7 @@ try {
|
|
|
48
48
|
'Phase: ' + (data.phase && data.phase.current || 0) + ' (' + (data.phase && data.phase.status || 'initialized') + ')',
|
|
49
49
|
'Next: ' + (data.nextAction || '/brain:new-project'),
|
|
50
50
|
'',
|
|
51
|
-
'Commands: /brain:new-project, /brain:discuss, /brain:plan, /brain:execute, /brain:verify, /brain:complete, /brain:quick, /brain:progress, /brain:pause, /brain:resume, /brain:help, /brain:health, /brain:update, /brain:storm, /brain:adr, /brain:phase, /brain:config, /brain:map, /brain:recover, /brain:dashboard, /brain:auto (or execute --auto)',
|
|
51
|
+
'Commands: /brain:new-project, /brain:discuss, /brain:plan, /brain:execute, /brain:verify, /brain:complete, /brain:quick, /brain:new-task, /brain:progress, /brain:pause, /brain:resume, /brain:help, /brain:health, /brain:update, /brain:storm, /brain:adr, /brain:phase, /brain:config, /brain:map, /brain:recover, /brain:dashboard, /brain:auto (or execute --auto)',
|
|
52
52
|
'',
|
|
53
53
|
'Instructions for Claude:',
|
|
54
54
|
'- When user types /brain:<command>, run: npx brain-dev <command> [args]',
|