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,142 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { readState, writeState, atomicWriteSync } = require('../state.cjs');
|
|
6
|
+
const { output, prefix, success, error } = require('../core.cjs');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Parse --note flag from args.
|
|
10
|
+
* @param {string[]} args
|
|
11
|
+
* @returns {string|null}
|
|
12
|
+
*/
|
|
13
|
+
function parseNote(args) {
|
|
14
|
+
const idx = args.indexOf('--note');
|
|
15
|
+
if (idx === -1 || idx + 1 >= args.length) return null;
|
|
16
|
+
return args[idx + 1];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Generate continue-here.md content from state.
|
|
21
|
+
* @param {object} state - brain.json state
|
|
22
|
+
* @param {string|null} note - Optional user note
|
|
23
|
+
* @returns {string} Markdown content
|
|
24
|
+
*/
|
|
25
|
+
function generateSnapshot(state, note) {
|
|
26
|
+
const phase = state.phase || {};
|
|
27
|
+
const phases = phase.phases || [];
|
|
28
|
+
const now = new Date().toISOString();
|
|
29
|
+
|
|
30
|
+
// YAML frontmatter
|
|
31
|
+
const lines = [
|
|
32
|
+
'---',
|
|
33
|
+
`phase: ${phase.current || 0}`,
|
|
34
|
+
`status: ${phase.status || 'initialized'}`,
|
|
35
|
+
`paused_at: ${now}`,
|
|
36
|
+
'---',
|
|
37
|
+
''
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
// Current State
|
|
41
|
+
lines.push('## Current State');
|
|
42
|
+
if (note) {
|
|
43
|
+
lines.push(note);
|
|
44
|
+
} else {
|
|
45
|
+
const phaseName = phases.find(p => p.number === phase.current)?.name || 'Unknown';
|
|
46
|
+
lines.push(`Phase ${phase.current} (${phaseName}): ${phase.status}`);
|
|
47
|
+
}
|
|
48
|
+
lines.push('');
|
|
49
|
+
|
|
50
|
+
// Completed Work
|
|
51
|
+
lines.push('## Completed Work');
|
|
52
|
+
const completedPhases = phases.filter(p => p.status === 'complete');
|
|
53
|
+
if (completedPhases.length > 0) {
|
|
54
|
+
for (const p of completedPhases) {
|
|
55
|
+
lines.push(`- Phase ${p.number} (${p.name}): ${p.plans?.done || 0}/${p.plans?.total || 0} plans`);
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
lines.push('- No phases completed yet');
|
|
59
|
+
}
|
|
60
|
+
lines.push('');
|
|
61
|
+
|
|
62
|
+
// Remaining Work
|
|
63
|
+
lines.push('## Remaining Work');
|
|
64
|
+
const remainingPhases = phases.filter(p => p.status !== 'complete');
|
|
65
|
+
if (remainingPhases.length > 0) {
|
|
66
|
+
for (const p of remainingPhases) {
|
|
67
|
+
const remaining = (p.plans?.total || 0) - (p.plans?.done || 0);
|
|
68
|
+
lines.push(`- Phase ${p.number} (${p.name}): ${remaining} plans remaining`);
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
lines.push('- All phases complete');
|
|
72
|
+
}
|
|
73
|
+
lines.push('');
|
|
74
|
+
|
|
75
|
+
// Decisions Made
|
|
76
|
+
lines.push('## Decisions Made');
|
|
77
|
+
lines.push('- See .brain/brain.json for full state');
|
|
78
|
+
lines.push('');
|
|
79
|
+
|
|
80
|
+
// Conversation Summary
|
|
81
|
+
lines.push('## Conversation Summary');
|
|
82
|
+
lines.push('<!-- Claude: summarize the current conversation context here when presenting to user -->');
|
|
83
|
+
lines.push('');
|
|
84
|
+
|
|
85
|
+
// Next Action
|
|
86
|
+
lines.push('## Next Action');
|
|
87
|
+
const { nextAction } = require('./progress.cjs');
|
|
88
|
+
lines.push(nextAction(state));
|
|
89
|
+
lines.push('');
|
|
90
|
+
|
|
91
|
+
return lines.join('\n');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Run the pause command.
|
|
96
|
+
* @param {string[]} args - CLI arguments
|
|
97
|
+
* @param {object} [opts] - Options (brainDir for testing)
|
|
98
|
+
* @returns {object} Result
|
|
99
|
+
*/
|
|
100
|
+
async function run(args = [], opts = {}) {
|
|
101
|
+
const brainDir = opts.brainDir || path.join(process.cwd(), '.brain');
|
|
102
|
+
const state = readState(brainDir);
|
|
103
|
+
|
|
104
|
+
if (!state) {
|
|
105
|
+
error("No brain state found. Run 'brain-dev init' first.");
|
|
106
|
+
return { error: 'no-state' };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const note = parseNote(args);
|
|
110
|
+
const snapshot = generateSnapshot(state, note);
|
|
111
|
+
const now = new Date().toISOString();
|
|
112
|
+
|
|
113
|
+
// Write continue-here.md
|
|
114
|
+
const snapshotPath = path.join(brainDir, 'continue-here.md');
|
|
115
|
+
atomicWriteSync(snapshotPath, snapshot);
|
|
116
|
+
|
|
117
|
+
// Archive to sessions/
|
|
118
|
+
const sessionsDir = path.join(brainDir, 'sessions');
|
|
119
|
+
if (!fs.existsSync(sessionsDir)) {
|
|
120
|
+
fs.mkdirSync(sessionsDir, { recursive: true });
|
|
121
|
+
}
|
|
122
|
+
const sessionFileName = now.replace(/:/g, '-').replace(/\.\d+Z$/, 'Z') + '.md';
|
|
123
|
+
// Simplify to safe filename
|
|
124
|
+
const safeFileName = now.slice(0, 19).replace(/:/g, '-') + '.md';
|
|
125
|
+
fs.copyFileSync(snapshotPath, path.join(sessionsDir, safeFileName));
|
|
126
|
+
|
|
127
|
+
// Update state
|
|
128
|
+
state.session = state.session || {};
|
|
129
|
+
state.session.lastPaused = now;
|
|
130
|
+
state.session.snapshotPath = '.brain/continue-here.md';
|
|
131
|
+
writeState(brainDir, state);
|
|
132
|
+
|
|
133
|
+
success('Session paused. Snapshot saved to .brain/continue-here.md');
|
|
134
|
+
output(
|
|
135
|
+
{ paused: true, snapshot: snapshotPath, session: safeFileName },
|
|
136
|
+
prefix(`Archived to .brain/sessions/${safeFileName}`)
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
return { paused: true, snapshot: snapshotPath };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
module.exports = { run };
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const { readState, writeState } = require('../state.cjs');
|
|
5
|
+
const { parseRoadmap, writeRoadmap, insertPhase, removePhase, reorderPhases } = require('../roadmap.cjs');
|
|
6
|
+
const { output, error, success } = require('../core.cjs');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Parse a named flag value from args array.
|
|
10
|
+
* @param {string[]} args
|
|
11
|
+
* @param {string} flag - Flag name (e.g. '--after')
|
|
12
|
+
* @returns {string|null}
|
|
13
|
+
*/
|
|
14
|
+
function getFlag(args, flag) {
|
|
15
|
+
const idx = args.indexOf(flag);
|
|
16
|
+
if (idx === -1 || idx === args.length - 1) return null;
|
|
17
|
+
return args[idx + 1];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Check if a flag exists in args (boolean flag).
|
|
22
|
+
* @param {string[]} args
|
|
23
|
+
* @param {string} flag
|
|
24
|
+
* @returns {boolean}
|
|
25
|
+
*/
|
|
26
|
+
function hasFlag(args, flag) {
|
|
27
|
+
return args.includes(flag);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Map phase status to a display indicator type.
|
|
32
|
+
* @param {string} status
|
|
33
|
+
* @returns {string} 'done' | 'current' | 'pending'
|
|
34
|
+
*/
|
|
35
|
+
function statusIndicator(status) {
|
|
36
|
+
const s = (status || '').toLowerCase();
|
|
37
|
+
if (s === 'complete' || s === 'completed' || s === 'done') return 'done';
|
|
38
|
+
if (s === 'in progress' || s === 'executing' || s === 'current') return 'current';
|
|
39
|
+
return 'pending';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Format phase number for display.
|
|
44
|
+
* @param {number} n
|
|
45
|
+
* @returns {string}
|
|
46
|
+
*/
|
|
47
|
+
function fmtNum(n) {
|
|
48
|
+
return Number.isInteger(n) ? String(n) : n.toFixed(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Check if a phase is completed (read-only).
|
|
53
|
+
* @param {{ status: string }} phase
|
|
54
|
+
* @returns {boolean}
|
|
55
|
+
*/
|
|
56
|
+
function isComplete(phase) {
|
|
57
|
+
return statusIndicator(phase.status) === 'done';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Handle the 'list' subcommand.
|
|
62
|
+
*/
|
|
63
|
+
function handleList(roadmap) {
|
|
64
|
+
const phases = roadmap.phases.map(p => ({
|
|
65
|
+
number: p.number,
|
|
66
|
+
name: p.name,
|
|
67
|
+
goal: p.goal,
|
|
68
|
+
status: p.status,
|
|
69
|
+
indicator: statusIndicator(p.status),
|
|
70
|
+
dependsOn: p.dependsOn,
|
|
71
|
+
requirements: p.requirements
|
|
72
|
+
}));
|
|
73
|
+
|
|
74
|
+
// Human-readable table
|
|
75
|
+
const indicatorSymbol = { done: '[x]', current: '[>]', pending: '[ ]' };
|
|
76
|
+
const lines = ['Phases:', ''];
|
|
77
|
+
for (const p of phases) {
|
|
78
|
+
const sym = indicatorSymbol[p.indicator];
|
|
79
|
+
lines.push(` ${sym} Phase ${fmtNum(p.number)}: ${p.name} (${p.status})`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
output({ phases }, lines.join('\n'));
|
|
83
|
+
return { phases };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Handle the 'add' subcommand.
|
|
88
|
+
*/
|
|
89
|
+
function handleAdd(args, roadmap, brainDir) {
|
|
90
|
+
const name = getFlag(args, '--name');
|
|
91
|
+
const goal = getFlag(args, '--goal');
|
|
92
|
+
const after = getFlag(args, '--after');
|
|
93
|
+
|
|
94
|
+
// If no name/goal provided, output instructions for Claude to ask user
|
|
95
|
+
if (!name || !goal) {
|
|
96
|
+
const questions = [
|
|
97
|
+
{ id: 'name', question: 'What is the name of the new phase?' },
|
|
98
|
+
{ id: 'goal', question: 'What is the goal of this phase?' },
|
|
99
|
+
{ id: 'after', question: `After which phase should it be added? (current phases: ${roadmap.phases.map(p => `${fmtNum(p.number)}: ${p.name}`).join(', ')})` },
|
|
100
|
+
{ id: 'requirements', question: 'Any requirements to assign? (comma-separated, or skip)' }
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
const result = {
|
|
104
|
+
action: 'ask-user',
|
|
105
|
+
command: 'phase add',
|
|
106
|
+
questions,
|
|
107
|
+
instructions: 'Use AskUserQuestion to ask each question, then call: brain-dev phase add --name "<name>" --goal "<goal>" --after <N>'
|
|
108
|
+
};
|
|
109
|
+
output(result, 'To add a phase, please provide: name, goal, position, and optional requirements.');
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Name and goal provided -- execute the add
|
|
114
|
+
const afterNum = after ? parseFloat(after) : roadmap.phases[roadmap.phases.length - 1]?.number || 0;
|
|
115
|
+
const reqStr = getFlag(args, '--requirements');
|
|
116
|
+
const requirements = reqStr ? reqStr.split(',').map(s => s.trim()) : [];
|
|
117
|
+
|
|
118
|
+
const updatedRoadmap = insertPhase(roadmap, afterNum, { name, goal, requirements });
|
|
119
|
+
writeRoadmap(brainDir, updatedRoadmap);
|
|
120
|
+
|
|
121
|
+
// Find the newly created phase
|
|
122
|
+
const newPhase = updatedRoadmap.phases.find(p => p.name === name);
|
|
123
|
+
|
|
124
|
+
// Update state
|
|
125
|
+
const state = readState(brainDir);
|
|
126
|
+
if (state) {
|
|
127
|
+
state.phase.total = updatedRoadmap.phases.length;
|
|
128
|
+
writeState(brainDir, state);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
success(`Phase ${fmtNum(newPhase.number)}: ${name} added.`);
|
|
132
|
+
return { action: 'added', phase: newPhase };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Handle the 'insert' subcommand.
|
|
137
|
+
*/
|
|
138
|
+
function handleInsert(args, roadmap, brainDir) {
|
|
139
|
+
const after = getFlag(args, '--after');
|
|
140
|
+
if (!after) {
|
|
141
|
+
error('Missing --after flag. Usage: brain-dev phase insert --after <N>');
|
|
142
|
+
return { error: 'missing-flag', message: 'Missing --after flag' };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const afterNum = parseFloat(after);
|
|
146
|
+
const afterPhase = roadmap.phases.find(p => p.number === afterNum);
|
|
147
|
+
if (!afterPhase) {
|
|
148
|
+
error(`Phase ${after} not found.`);
|
|
149
|
+
return { error: 'not-found', message: `Phase ${after} not found` };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const name = getFlag(args, '--name');
|
|
153
|
+
const goal = getFlag(args, '--goal');
|
|
154
|
+
|
|
155
|
+
// If no name/goal, output instructions for Claude
|
|
156
|
+
if (!name || !goal) {
|
|
157
|
+
const result = {
|
|
158
|
+
action: 'ask-user',
|
|
159
|
+
command: 'phase insert',
|
|
160
|
+
insertAfter: afterNum,
|
|
161
|
+
questions: [
|
|
162
|
+
{ id: 'name', question: 'What is the name of the new phase?' },
|
|
163
|
+
{ id: 'goal', question: 'What is the goal of this phase?' },
|
|
164
|
+
{ id: 'requirements', question: 'Any requirements to assign? (comma-separated, or skip)' }
|
|
165
|
+
],
|
|
166
|
+
instructions: `Use AskUserQuestion to ask each question, then call: brain-dev phase insert --after ${after} --name "<name>" --goal "<goal>"`
|
|
167
|
+
};
|
|
168
|
+
output(result, `To insert a phase after ${fmtNum(afterNum)}, please provide: name, goal, and optional requirements.`);
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Execute the insert
|
|
173
|
+
const reqStr = getFlag(args, '--requirements');
|
|
174
|
+
const requirements = reqStr ? reqStr.split(',').map(s => s.trim()) : [];
|
|
175
|
+
|
|
176
|
+
const updatedRoadmap = insertPhase(roadmap, afterNum, { name, goal, requirements });
|
|
177
|
+
writeRoadmap(brainDir, updatedRoadmap);
|
|
178
|
+
|
|
179
|
+
const newPhase = updatedRoadmap.phases.find(p => p.name === name);
|
|
180
|
+
|
|
181
|
+
// Update state
|
|
182
|
+
const state = readState(brainDir);
|
|
183
|
+
if (state) {
|
|
184
|
+
state.phase.total = updatedRoadmap.phases.length;
|
|
185
|
+
writeState(brainDir, state);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
success(`Phase ${fmtNum(newPhase.number)}: ${name} inserted after phase ${fmtNum(afterNum)}.`);
|
|
189
|
+
return { action: 'inserted', phase: newPhase };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Handle the 'remove' subcommand.
|
|
194
|
+
*/
|
|
195
|
+
function handleRemove(args, roadmap, brainDir) {
|
|
196
|
+
const phaseStr = getFlag(args, '--phase');
|
|
197
|
+
if (!phaseStr) {
|
|
198
|
+
error('Missing --phase flag. Usage: brain-dev phase remove --phase <N>');
|
|
199
|
+
return { error: 'missing-flag', message: 'Missing --phase flag' };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const phaseNum = parseFloat(phaseStr);
|
|
203
|
+
const targetPhase = roadmap.phases.find(p => p.number === phaseNum);
|
|
204
|
+
|
|
205
|
+
if (!targetPhase) {
|
|
206
|
+
error(`Phase ${phaseStr} not found.`);
|
|
207
|
+
return { error: 'not-found', message: `Phase ${phaseStr} not found` };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Completed phases are read-only
|
|
211
|
+
if (isComplete(targetPhase)) {
|
|
212
|
+
error(`Phase ${fmtNum(phaseNum)}: ${targetPhase.name} is complete and read-only. Create a new gap closure phase instead.`);
|
|
213
|
+
return {
|
|
214
|
+
error: 'read-only',
|
|
215
|
+
message: `Phase ${fmtNum(phaseNum)} is complete and cannot be modified. Create a new gap closure phase instead.`
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const confirm = hasFlag(args, '--confirm');
|
|
220
|
+
|
|
221
|
+
// First call: show orphaned requirements and ask for confirmation
|
|
222
|
+
if (!confirm) {
|
|
223
|
+
const { orphanedRequirements } = removePhase(roadmap, phaseNum);
|
|
224
|
+
const result = {
|
|
225
|
+
action: 'ask-user',
|
|
226
|
+
command: 'phase remove',
|
|
227
|
+
phaseToRemove: phaseNum,
|
|
228
|
+
phaseName: targetPhase.name,
|
|
229
|
+
orphanedRequirements,
|
|
230
|
+
questions: orphanedRequirements.length > 0
|
|
231
|
+
? orphanedRequirements.map(req => ({
|
|
232
|
+
id: `requirement-${req}`,
|
|
233
|
+
question: `Requirement ${req} will be orphaned. Reassign to another phase / Delete / Hold as unassigned?`
|
|
234
|
+
}))
|
|
235
|
+
: [],
|
|
236
|
+
instructions: orphanedRequirements.length > 0
|
|
237
|
+
? `Phase ${fmtNum(phaseNum)} has ${orphanedRequirements.length} orphaned requirement(s). Ask user about each, then call: brain-dev phase remove --phase ${phaseStr} --confirm`
|
|
238
|
+
: `Phase ${fmtNum(phaseNum)} has no requirements. Confirm removal: brain-dev phase remove --phase ${phaseStr} --confirm`
|
|
239
|
+
};
|
|
240
|
+
output(result, `Phase ${fmtNum(phaseNum)}: ${targetPhase.name} will be removed.`);
|
|
241
|
+
return result;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Confirmed: execute removal
|
|
245
|
+
const { roadmap: updatedRoadmap, orphanedRequirements } = removePhase(roadmap, phaseNum);
|
|
246
|
+
writeRoadmap(brainDir, updatedRoadmap);
|
|
247
|
+
|
|
248
|
+
// Update state
|
|
249
|
+
const state = readState(brainDir);
|
|
250
|
+
if (state) {
|
|
251
|
+
state.phase.total = updatedRoadmap.phases.length;
|
|
252
|
+
writeState(brainDir, state);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
success(`Phase ${fmtNum(phaseNum)} removed. Dependencies updated.`);
|
|
256
|
+
return { action: 'removed', removed: phaseNum, orphanedRequirements };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Handle the 'reorder' subcommand.
|
|
261
|
+
*/
|
|
262
|
+
function handleReorder(args, roadmap, brainDir) {
|
|
263
|
+
const orderStr = getFlag(args, '--order');
|
|
264
|
+
|
|
265
|
+
// No order provided: show current order and ask
|
|
266
|
+
if (!orderStr) {
|
|
267
|
+
const currentOrder = roadmap.phases.map(p => ({
|
|
268
|
+
number: p.number,
|
|
269
|
+
name: p.name,
|
|
270
|
+
status: p.status,
|
|
271
|
+
indicator: statusIndicator(p.status)
|
|
272
|
+
}));
|
|
273
|
+
|
|
274
|
+
const result = {
|
|
275
|
+
action: 'ask-user',
|
|
276
|
+
command: 'phase reorder',
|
|
277
|
+
currentOrder,
|
|
278
|
+
instructions: `Current order: ${currentOrder.map(p => `${fmtNum(p.number)}: ${p.name}`).join(', ')}. Ask user for new order, then call: brain-dev phase reorder --order "N,N,N,..."`
|
|
279
|
+
};
|
|
280
|
+
output(result, 'Current phase order:\n' + currentOrder.map(p => ` ${fmtNum(p.number)}: ${p.name} (${p.status})`).join('\n'));
|
|
281
|
+
return result;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Parse new order
|
|
285
|
+
const newOrder = orderStr.split(',').map(s => parseFloat(s.trim()));
|
|
286
|
+
|
|
287
|
+
// Validate: completed phases must remain in their original position
|
|
288
|
+
// Find completed phases and check they are in the same relative position
|
|
289
|
+
const completedPhases = roadmap.phases.filter(p => isComplete(p));
|
|
290
|
+
for (const cp of completedPhases) {
|
|
291
|
+
const originalIdx = roadmap.phases.indexOf(cp);
|
|
292
|
+
const newIdx = newOrder.indexOf(cp.number);
|
|
293
|
+
if (newIdx !== originalIdx) {
|
|
294
|
+
error(`Phase ${fmtNum(cp.number)}: ${cp.name} is complete and cannot be moved. Completed phases are read-only.`);
|
|
295
|
+
return {
|
|
296
|
+
error: 'read-only',
|
|
297
|
+
message: `Phase ${fmtNum(cp.number)} is complete and cannot be moved. Completed phases are read-only.`
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const updatedRoadmap = reorderPhases(roadmap, newOrder);
|
|
303
|
+
writeRoadmap(brainDir, updatedRoadmap);
|
|
304
|
+
|
|
305
|
+
// Update state
|
|
306
|
+
const state = readState(brainDir);
|
|
307
|
+
if (state) {
|
|
308
|
+
state.phase.total = updatedRoadmap.phases.length;
|
|
309
|
+
writeState(brainDir, state);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
success('Phases reordered. Dependencies updated.');
|
|
313
|
+
return { action: 'reordered', newOrder: updatedRoadmap.phases.map(p => ({ number: p.number, name: p.name })) };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Run the phase management command.
|
|
318
|
+
* @param {string[]} args - CLI arguments (subcommand + flags)
|
|
319
|
+
* @param {object} [opts] - Options (brainDir for testing)
|
|
320
|
+
* @returns {object} Structured result
|
|
321
|
+
*/
|
|
322
|
+
async function run(args = [], opts = {}) {
|
|
323
|
+
const brainDir = opts.brainDir || path.join(process.cwd(), '.brain');
|
|
324
|
+
const subcommand = args[0] || 'list';
|
|
325
|
+
|
|
326
|
+
// Read roadmap
|
|
327
|
+
let roadmap;
|
|
328
|
+
try {
|
|
329
|
+
roadmap = parseRoadmap(brainDir);
|
|
330
|
+
} catch (e) {
|
|
331
|
+
error(`Cannot read roadmap: ${e.message}`);
|
|
332
|
+
return { error: 'no-roadmap', message: e.message };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
switch (subcommand) {
|
|
336
|
+
case 'list':
|
|
337
|
+
return handleList(roadmap);
|
|
338
|
+
|
|
339
|
+
case 'add':
|
|
340
|
+
return handleAdd(args, roadmap, brainDir);
|
|
341
|
+
|
|
342
|
+
case 'insert':
|
|
343
|
+
return handleInsert(args, roadmap, brainDir);
|
|
344
|
+
|
|
345
|
+
case 'remove':
|
|
346
|
+
return handleRemove(args, roadmap, brainDir);
|
|
347
|
+
|
|
348
|
+
case 'reorder':
|
|
349
|
+
return handleReorder(args, roadmap, brainDir);
|
|
350
|
+
|
|
351
|
+
default:
|
|
352
|
+
error(`Unknown subcommand: '${subcommand}'. Use: list, add, insert, remove, reorder`);
|
|
353
|
+
return { error: 'unknown-subcommand', message: `Unknown subcommand: ${subcommand}` };
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
module.exports = { run };
|