brain-dev 0.1.2 → 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 +21 -1
- package/bin/lib/bridge.cjs +47 -0
- package/bin/lib/commands/auto.cjs +337 -0
- package/bin/lib/commands/dashboard.cjs +177 -0
- package/bin/lib/commands/execute.cjs +18 -0
- package/bin/lib/commands/new-task.cjs +522 -0
- package/bin/lib/commands/progress.cjs +37 -1
- package/bin/lib/commands/recover.cjs +155 -0
- package/bin/lib/commands/verify.cjs +15 -5
- package/bin/lib/commands.cjs +27 -0
- package/bin/lib/config.cjs +23 -2
- package/bin/lib/context.cjs +397 -0
- package/bin/lib/cost.cjs +273 -0
- package/bin/lib/dashboard-collector.cjs +98 -0
- package/bin/lib/dashboard-server.cjs +33 -0
- package/bin/lib/hook-dispatcher.cjs +99 -0
- package/bin/lib/lock.cjs +163 -0
- package/bin/lib/logger.cjs +18 -0
- package/bin/lib/recovery.cjs +468 -0
- package/bin/lib/security.cjs +16 -2
- package/bin/lib/state.cjs +145 -8
- package/bin/lib/stuck.cjs +269 -0
- package/bin/lib/tokens.cjs +32 -0
- package/commands/brain/auto.md +31 -0
- package/commands/brain/dashboard.md +18 -0
- package/commands/brain/new-task.md +31 -0
- package/commands/brain/recover.md +19 -0
- package/hooks/bootstrap.sh +15 -1
- package/package.json +1 -1
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { parseArgs } = require('node:util');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { output, error, success, prefix } = require('../core.cjs');
|
|
6
|
+
const { readState } = require('../state.cjs');
|
|
7
|
+
const { readLock, isLockStale } = require('../lock.cjs');
|
|
8
|
+
const recovery = require('../recovery.cjs');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Recover command handler.
|
|
12
|
+
* Detects and recovers from crashed/interrupted executions.
|
|
13
|
+
*
|
|
14
|
+
* Flags:
|
|
15
|
+
* --fix Attempt auto-resume from last checkpoint
|
|
16
|
+
* --rollback Revert state to pre-execution
|
|
17
|
+
* --dismiss Clear stale lock without recovery
|
|
18
|
+
* --json Force JSON output
|
|
19
|
+
*
|
|
20
|
+
* Default (no flags): check mode — show recovery briefing
|
|
21
|
+
*
|
|
22
|
+
* @param {string[]} args - CLI arguments
|
|
23
|
+
* @param {object} [opts] - Options (brainDir override)
|
|
24
|
+
* @returns {object} Result object
|
|
25
|
+
*/
|
|
26
|
+
async function run(args = [], opts = {}) {
|
|
27
|
+
const { values } = parseArgs({
|
|
28
|
+
args,
|
|
29
|
+
options: {
|
|
30
|
+
fix: { type: 'boolean', default: false },
|
|
31
|
+
rollback: { type: 'boolean', default: false },
|
|
32
|
+
dismiss: { type: 'boolean', default: false },
|
|
33
|
+
json: { type: 'boolean', default: false }
|
|
34
|
+
},
|
|
35
|
+
strict: false
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const brainDir = opts.brainDir || path.join(process.cwd(), '.brain');
|
|
39
|
+
|
|
40
|
+
// Validate brainDir exists
|
|
41
|
+
const state = readState(brainDir);
|
|
42
|
+
if (!state) {
|
|
43
|
+
const result = { action: 'recover', status: 'error', message: 'No .brain directory or brain.json found' };
|
|
44
|
+
if (values.json) {
|
|
45
|
+
console.log(JSON.stringify(result));
|
|
46
|
+
} else {
|
|
47
|
+
error('No .brain directory found. Run `brain init` first.');
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Mutually exclusive flags check
|
|
53
|
+
const flagCount = [values.fix, values.rollback, values.dismiss].filter(Boolean).length;
|
|
54
|
+
if (flagCount > 1) {
|
|
55
|
+
const result = { action: 'recover', status: 'error', message: 'Use only one of --fix, --rollback, or --dismiss' };
|
|
56
|
+
if (values.json) {
|
|
57
|
+
console.log(JSON.stringify(result));
|
|
58
|
+
} else {
|
|
59
|
+
error('Use only one of --fix, --rollback, or --dismiss.');
|
|
60
|
+
}
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Dismiss mode: clear stale lock without recovery
|
|
65
|
+
if (values.dismiss) {
|
|
66
|
+
const result = recovery.recover(brainDir, { dismiss: true });
|
|
67
|
+
const resultObj = { action: 'recover', status: result.recovered ? 'dismissed' : 'no-action', ...result };
|
|
68
|
+
|
|
69
|
+
if (values.json) {
|
|
70
|
+
console.log(JSON.stringify(resultObj, null, 2));
|
|
71
|
+
} else if (result.recovered) {
|
|
72
|
+
success('Stale lock dismissed. No state changes made.');
|
|
73
|
+
} else {
|
|
74
|
+
output(resultObj, prefix(result.briefing));
|
|
75
|
+
}
|
|
76
|
+
return resultObj;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Fix mode: auto-resume recovery
|
|
80
|
+
if (values.fix) {
|
|
81
|
+
const result = recovery.recover(brainDir, { fix: true });
|
|
82
|
+
const resultObj = { action: 'recover', status: result.recovered ? 'fixed' : 'no-action', ...result };
|
|
83
|
+
|
|
84
|
+
if (values.json) {
|
|
85
|
+
console.log(JSON.stringify(resultObj, null, 2));
|
|
86
|
+
} else if (result.recovered) {
|
|
87
|
+
// Show briefing then success
|
|
88
|
+
console.log('');
|
|
89
|
+
console.log(result.briefing);
|
|
90
|
+
console.log('');
|
|
91
|
+
success(`Recovery complete. ${result.nextAction}`);
|
|
92
|
+
if (result.result?.changes) {
|
|
93
|
+
for (const change of result.result.changes) {
|
|
94
|
+
console.log(prefix(` - ${change}`));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
} else {
|
|
98
|
+
output(resultObj, prefix(result.briefing));
|
|
99
|
+
}
|
|
100
|
+
return resultObj;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Rollback mode: revert state
|
|
104
|
+
if (values.rollback) {
|
|
105
|
+
const result = recovery.recover(brainDir, { rollback: true });
|
|
106
|
+
const resultObj = { action: 'recover', status: result.recovered ? 'rolled-back' : 'no-action', ...result };
|
|
107
|
+
|
|
108
|
+
if (values.json) {
|
|
109
|
+
console.log(JSON.stringify(resultObj, null, 2));
|
|
110
|
+
} else if (result.recovered) {
|
|
111
|
+
console.log('');
|
|
112
|
+
console.log(result.briefing);
|
|
113
|
+
console.log('');
|
|
114
|
+
success(`Rollback complete. ${result.nextAction}`);
|
|
115
|
+
if (result.result?.changes) {
|
|
116
|
+
for (const change of result.result.changes) {
|
|
117
|
+
console.log(prefix(` - ${change}`));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
output(resultObj, prefix(result.briefing));
|
|
122
|
+
}
|
|
123
|
+
return resultObj;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Default: check mode — show briefing without making changes
|
|
127
|
+
const result = recovery.recover(brainDir, {});
|
|
128
|
+
const resultObj = { action: 'recover', status: result.mode, ...result };
|
|
129
|
+
|
|
130
|
+
if (values.json) {
|
|
131
|
+
console.log(JSON.stringify(resultObj, null, 2));
|
|
132
|
+
} else {
|
|
133
|
+
console.log('');
|
|
134
|
+
console.log(result.briefing);
|
|
135
|
+
console.log('');
|
|
136
|
+
|
|
137
|
+
if (result.mode === 'clean' || result.mode === 'active' || result.mode === 'none') {
|
|
138
|
+
success(result.briefing);
|
|
139
|
+
} else {
|
|
140
|
+
// There is something to recover
|
|
141
|
+
const recommendation = result.result?.recommended;
|
|
142
|
+
if (recommendation === 'auto-resume') {
|
|
143
|
+
console.log(prefix('Suggested: brain recover --fix'));
|
|
144
|
+
} else if (recommendation === 'rollback') {
|
|
145
|
+
console.log(prefix('Suggested: brain recover --rollback'));
|
|
146
|
+
} else {
|
|
147
|
+
console.log(prefix('Review the briefing above and choose: --fix, --rollback, or --dismiss'));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return resultObj;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
module.exports = { run };
|
|
@@ -9,6 +9,7 @@ const { logEvent } = require('../logger.cjs');
|
|
|
9
9
|
const { output, error, success } = require('../core.cjs');
|
|
10
10
|
const antiPatterns = require('../anti-patterns.cjs');
|
|
11
11
|
const { buildDebuggerSpawnInstructions } = require('./execute.cjs');
|
|
12
|
+
const { isPathWithinRoot } = require('../security.cjs');
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Find a phase directory under .brain/phases/ matching a phase number.
|
|
@@ -72,9 +73,10 @@ function extractMustHaves(content) {
|
|
|
72
73
|
* Extract phase-modified file paths from PLAN and SUMMARY frontmatter.
|
|
73
74
|
* @param {string[]} planFiles - Full paths to plan files
|
|
74
75
|
* @param {string[]} summaryFiles - Full paths to summary files
|
|
76
|
+
* @param {string} [rootDir] - Project root directory for path traversal guard
|
|
75
77
|
* @returns {string[]} Deduplicated array of file paths
|
|
76
78
|
*/
|
|
77
|
-
function extractPhaseFiles(planFiles, summaryFiles) {
|
|
79
|
+
function extractPhaseFiles(planFiles, summaryFiles, rootDir) {
|
|
78
80
|
const files = new Set();
|
|
79
81
|
|
|
80
82
|
for (const planPath of planFiles) {
|
|
@@ -90,7 +92,10 @@ function extractPhaseFiles(planFiles, summaryFiles) {
|
|
|
90
92
|
for (const line of lines) {
|
|
91
93
|
const itemMatch = line.match(/^\s+-\s+(.+)/);
|
|
92
94
|
if (itemMatch) {
|
|
93
|
-
|
|
95
|
+
const filePath = itemMatch[1].trim();
|
|
96
|
+
// Guard against path traversal
|
|
97
|
+
if (rootDir && !isPathWithinRoot(filePath, rootDir)) continue;
|
|
98
|
+
files.add(filePath);
|
|
94
99
|
} else if (line.trim() && !line.match(/^\s+-/) && !line.match(/^\s*$/)) {
|
|
95
100
|
break; // Hit next YAML key
|
|
96
101
|
}
|
|
@@ -117,7 +122,10 @@ function extractPhaseFiles(planFiles, summaryFiles) {
|
|
|
117
122
|
for (const line of lines) {
|
|
118
123
|
const itemMatch = line.match(/^\s+-\s+(.+)/);
|
|
119
124
|
if (itemMatch) {
|
|
120
|
-
|
|
125
|
+
const filePath = itemMatch[1].trim().replace(/^["']|["']$/g, '');
|
|
126
|
+
// Guard against path traversal
|
|
127
|
+
if (rootDir && !isPathWithinRoot(filePath, rootDir)) continue;
|
|
128
|
+
files.add(filePath);
|
|
121
129
|
} else if (line.trim() && !line.match(/^\s+-/) && !line.match(/^\s*$/) && !line.match(/^\s+\w+:/)) {
|
|
122
130
|
break;
|
|
123
131
|
}
|
|
@@ -213,11 +221,11 @@ async function run(args = [], opts = {}) {
|
|
|
213
221
|
const summaryPaths = summaryFiles.map(f => path.join(phaseDir, f));
|
|
214
222
|
|
|
215
223
|
// Extract phase-modified files for anti-pattern scanning
|
|
224
|
+
const projectDir = path.dirname(brainDir);
|
|
216
225
|
const planFullPaths = planFiles.map(f => path.join(phaseDir, f));
|
|
217
|
-
const phaseFiles = extractPhaseFiles(planFullPaths, summaryPaths);
|
|
226
|
+
const phaseFiles = extractPhaseFiles(planFullPaths, summaryPaths, projectDir);
|
|
218
227
|
|
|
219
228
|
// Run anti-pattern scan on phase files
|
|
220
|
-
const projectDir = path.dirname(brainDir);
|
|
221
229
|
const apResults = antiPatterns.scanFiles(projectDir, { files: phaseFiles });
|
|
222
230
|
|
|
223
231
|
// Build Nyquist section based on state config
|
|
@@ -400,6 +408,8 @@ function handleSaveResults(args, saveIdx, brainDir, state) {
|
|
|
400
408
|
|
|
401
409
|
// Update state
|
|
402
410
|
state.phase.status = results.passed ? 'verified' : 'verification-failed';
|
|
411
|
+
// Clear execution timer after verification completes
|
|
412
|
+
state.phase.execution_started_at = null;
|
|
403
413
|
writeState(brainDir, state);
|
|
404
414
|
|
|
405
415
|
const msg = results.passed
|
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',
|
|
@@ -148,6 +157,15 @@ const COMMANDS = [
|
|
|
148
157
|
needsState: true,
|
|
149
158
|
args: ' --session <id> Resume a specific session by timestamp ID'
|
|
150
159
|
},
|
|
160
|
+
{
|
|
161
|
+
name: 'recover',
|
|
162
|
+
description: 'Detect and recover from crashes',
|
|
163
|
+
usage: 'brain-dev recover [--fix|--rollback|--dismiss]',
|
|
164
|
+
group: 'Session',
|
|
165
|
+
implemented: true,
|
|
166
|
+
needsState: true,
|
|
167
|
+
args: ' --fix Auto-resume from last checkpoint\n --rollback Revert to pre-crash state\n --dismiss Clear stale lock, move on'
|
|
168
|
+
},
|
|
151
169
|
// Tools
|
|
152
170
|
{
|
|
153
171
|
name: 'storm',
|
|
@@ -158,6 +176,15 @@ const COMMANDS = [
|
|
|
158
176
|
needsState: true,
|
|
159
177
|
args: ' <topic> Topic to brainstorm\n --stop Stop running server\n --port <n> Custom port number\n --finalize Generate output.md from fragments'
|
|
160
178
|
},
|
|
179
|
+
{
|
|
180
|
+
name: 'dashboard',
|
|
181
|
+
description: 'Live monitoring dashboard',
|
|
182
|
+
usage: 'brain-dev dashboard [--browser|--stop|--status]',
|
|
183
|
+
group: 'Tools',
|
|
184
|
+
implemented: true,
|
|
185
|
+
needsState: true,
|
|
186
|
+
args: ' --browser Open web dashboard in browser\n --stop Stop running dashboard server\n --status Show dashboard URL if running'
|
|
187
|
+
},
|
|
161
188
|
{
|
|
162
189
|
name: 'adr',
|
|
163
190
|
description: 'Manage architecture decision records',
|
package/bin/lib/config.cjs
CHANGED
|
@@ -7,10 +7,10 @@ const { readState, writeState, atomicWriteSync } = require('./state.cjs');
|
|
|
7
7
|
|
|
8
8
|
// ─── Constants ──────────────────────────────────────────────────────────────
|
|
9
9
|
|
|
10
|
-
const CATEGORIES = ['Workflow', 'Models', 'Enforcement', 'Monitoring', 'Complexity', 'Storm', 'ADR'];
|
|
10
|
+
const CATEGORIES = ['Workflow', 'Models', 'Enforcement', 'Monitoring', 'Complexity', 'Storm', 'ADR', 'Budget', 'Timeout', 'Dashboard'];
|
|
11
11
|
|
|
12
12
|
const SCHEMA = {
|
|
13
|
-
'mode': { type: 'enum', values: ['interactive', 'yolo'], default: 'interactive', category: 'Workflow', description: 'Execution mode preset (sets multiple flags)' },
|
|
13
|
+
'mode': { type: 'enum', values: ['interactive', 'yolo', 'auto'], default: 'interactive', category: 'Workflow', description: 'Execution mode preset (sets multiple flags)' },
|
|
14
14
|
'depth': { type: 'enum', values: ['shallow', 'standard', 'deep'], default: 'deep', category: 'Workflow', description: 'Verification depth (shallow=L1, standard=L1+L2, deep=L1+L2+L3)' },
|
|
15
15
|
'workflow.parallelization': { type: 'boolean', default: false, category: 'Workflow', description: 'Enable parallel wave execution' },
|
|
16
16
|
'workflow.mapper_parallelization': { type: 'boolean', default: true, category: 'Workflow', description: 'Enable parallel codebase mapping' },
|
|
@@ -29,6 +29,21 @@ const SCHEMA = {
|
|
|
29
29
|
'storm.auto_open': { type: 'boolean', default: true, category: 'Storm', description: 'Auto-open browser on storm start', local: true },
|
|
30
30
|
'adr.auto_create': { type: 'boolean', default: true, category: 'ADR', description: 'Auto-create ADRs for significant decisions' },
|
|
31
31
|
'adr.status_lifecycle': { type: 'boolean', default: true, category: 'ADR', description: 'Enable ADR status transitions' },
|
|
32
|
+
'budget.ceiling_usd': { type: 'number', default: null, min: 0, category: 'Budget', description: 'Maximum spend in USD (null = unlimited)' },
|
|
33
|
+
'budget.warn_at_pct': { type: 'number', default: 80, min: 10, max: 100, category: 'Budget', description: 'Warn when this % of ceiling is reached' },
|
|
34
|
+
'budget.mode': { type: 'enum', values: ['warn', 'pause', 'hard-stop'], default: 'warn', category: 'Budget', description: 'Action when budget exceeded' },
|
|
35
|
+
'timeout.enabled': { type: 'boolean', default: true, category: 'Timeout', description: 'Enable stuck detection and timeouts' },
|
|
36
|
+
'timeout.soft': { type: 'number', default: 20, min: 5, max: 120, category: 'Timeout', description: 'Soft timeout in minutes (warning)' },
|
|
37
|
+
'timeout.idle': { type: 'number', default: 5, min: 1, max: 30, category: 'Timeout', description: 'Idle timeout in minutes (no progress)' },
|
|
38
|
+
'timeout.hard': { type: 'number', default: 45, min: 10, max: 180, category: 'Timeout', description: 'Hard timeout in minutes (force wrap-up)' },
|
|
39
|
+
'timeout.max_retries': { type: 'number', default: 1, min: 0, max: 3, category: 'Timeout', description: 'Max resume attempts after stuck timeout' },
|
|
40
|
+
'dashboard.port': { type: 'number', default: 3457, min: 1024, max: 65535, category: 'Dashboard', description: 'Dashboard server port' },
|
|
41
|
+
'dashboard.auto_open': { type: 'boolean', default: true, category: 'Dashboard', description: 'Auto-open browser on dashboard start' },
|
|
42
|
+
'auto.max_errors': { type: 'number', default: 3, min: 1, max: 10, category: 'Workflow', description: 'Max consecutive errors before auto mode pauses' },
|
|
43
|
+
'auto.max_phases': { type: 'number', default: 0, min: 0, max: 100, category: 'Workflow', description: 'Max phases to process in auto mode (0 = unlimited)' },
|
|
44
|
+
'auto.skip_discuss': { type: 'boolean', default: false, category: 'Workflow', description: 'Skip discuss step in auto mode when CONTEXT.md exists' },
|
|
45
|
+
'auto.max_operations': { type: 'number', default: 100, min: 10, max: 1000, category: 'Workflow', description: 'Max operations (commits/writes) per auto session' },
|
|
46
|
+
'auto.max_duration_minutes': { type: 'number', default: 120, min: 10, max: 480, category: 'Workflow', description: 'Max auto session duration in minutes' },
|
|
32
47
|
};
|
|
33
48
|
|
|
34
49
|
const PROFILES = {
|
|
@@ -69,6 +84,12 @@ const MODE_PRESETS = {
|
|
|
69
84
|
'workflow.auto_recover': false,
|
|
70
85
|
'workflow.parallelization': false,
|
|
71
86
|
'enforcement.level': 'hard'
|
|
87
|
+
},
|
|
88
|
+
auto: {
|
|
89
|
+
'workflow.advocate': false,
|
|
90
|
+
'workflow.auto_recover': true,
|
|
91
|
+
'workflow.parallelization': false,
|
|
92
|
+
'enforcement.level': 'balanced'
|
|
72
93
|
}
|
|
73
94
|
};
|
|
74
95
|
|