brain-dev 0.1.1 → 0.2.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.
@@ -133,7 +133,11 @@ async function main() {
133
133
  break;
134
134
 
135
135
  case 'execute':
136
- await require('./lib/commands/execute.cjs').run(args.slice(1));
136
+ if (args.includes('--auto') || args.includes('--dry-run') || args.includes('--stop')) {
137
+ await require('./lib/commands/auto.cjs').run(args.slice(1));
138
+ } else {
139
+ await require('./lib/commands/execute.cjs').run(args.slice(1));
140
+ }
137
141
  break;
138
142
 
139
143
  case 'verify':
@@ -162,6 +166,10 @@ async function main() {
162
166
  await require('./lib/commands/config.cjs').run(args.slice(1), path.join(process.cwd(), '.brain'));
163
167
  break;
164
168
 
169
+ case 'update':
170
+ await require('./lib/commands/update.cjs').run(args.slice(1));
171
+ break;
172
+
165
173
  case 'health':
166
174
  await require('./lib/commands/health.cjs').run(args.slice(1));
167
175
  break;
@@ -170,6 +178,18 @@ async function main() {
170
178
  await require('./lib/commands/map.cjs').run(args.slice(1));
171
179
  break;
172
180
 
181
+ case 'recover':
182
+ await require('./lib/commands/recover.cjs').run(args.slice(1));
183
+ break;
184
+
185
+ case 'dashboard':
186
+ await require('./lib/commands/dashboard.cjs').run(args.slice(1));
187
+ break;
188
+
189
+ case 'auto':
190
+ await require('./lib/commands/auto.cjs').run(args.slice(1));
191
+ break;
192
+
173
193
  default:
174
194
  // Unimplemented command: show full help text
175
195
  if (!cmdEntry.implemented) {
@@ -0,0 +1,47 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { atomicWriteSync } = require('./state.cjs');
6
+
7
+ /**
8
+ * Write a bridge file atomically.
9
+ * Bridge files are ephemeral JSON files for inter-process communication.
10
+ * Convention: .brain/.<name>-bridge.json
11
+ * @param {string} brainDir - Path to .brain/ directory
12
+ * @param {string} name - Bridge name (e.g., 'context', 'stuck', 'cost')
13
+ * @param {object} data - JSON-serializable data
14
+ */
15
+ function writeBridge(brainDir, name, data) {
16
+ const filePath = path.join(brainDir, `.${name}-bridge.json`);
17
+ atomicWriteSync(filePath, JSON.stringify(data, null, 2));
18
+ }
19
+
20
+ /**
21
+ * Read a bridge file. Returns null on any error (missing, corrupt, etc.).
22
+ * @param {string} brainDir
23
+ * @param {string} name
24
+ * @returns {object|null}
25
+ */
26
+ function readBridge(brainDir, name) {
27
+ try {
28
+ const filePath = path.join(brainDir, `.${name}-bridge.json`);
29
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Remove a bridge file.
37
+ * @param {string} brainDir
38
+ * @param {string} name
39
+ */
40
+ function cleanBridge(brainDir, name) {
41
+ try {
42
+ const filePath = path.join(brainDir, `.${name}-bridge.json`);
43
+ fs.unlinkSync(filePath);
44
+ } catch { /* ignore if already missing */ }
45
+ }
46
+
47
+ module.exports = { writeBridge, readBridge, cleanBridge };
@@ -0,0 +1,337 @@
1
+ 'use strict';
2
+
3
+ const { parseArgs } = require('node:util');
4
+ const path = require('node:path');
5
+ const fs = require('node:fs');
6
+ const { output, error, success, prefix } = require('../core.cjs');
7
+ const { readState, writeState } = require('../state.cjs');
8
+ const { acquireLock, releaseLock } = require('../lock.cjs');
9
+ const { canSpend } = require('../cost.cjs');
10
+ const { checkTimeouts } = require('../stuck.cjs');
11
+ const { logEvent } = require('../logger.cjs');
12
+
13
+ /**
14
+ * Run the auto command — the autonomous execution engine.
15
+ *
16
+ * Generates a "runbook" — a sequence of steps that tells Claude Code
17
+ * how to chain the agent calls. Auto does NOT directly execute agents.
18
+ * It calls the existing command modules (plan, execute, verify, complete)
19
+ * which generate prompts. Auto chains these prompts into a single
20
+ * compound instruction.
21
+ *
22
+ * Flags:
23
+ * --dry-run Show the runbook without acquiring a lock or modifying state
24
+ * --stop Stop auto mode, release lock, record history
25
+ * --phase N Start from a specific phase number
26
+ * --json Force JSON output
27
+ *
28
+ * @param {string[]} args - CLI arguments
29
+ * @param {object} [opts] - Options (brainDir for testing)
30
+ * @returns {object} Structured result
31
+ */
32
+ async function run(args = [], opts = {}) {
33
+ const { values } = parseArgs({
34
+ args,
35
+ options: {
36
+ 'dry-run': { type: 'boolean', default: false },
37
+ stop: { type: 'boolean', default: false },
38
+ phase: { type: 'string' },
39
+ json: { type: 'boolean', default: false }
40
+ },
41
+ strict: false
42
+ });
43
+
44
+ const brainDir = opts.brainDir || path.join(process.cwd(), '.brain');
45
+ const state = readState(brainDir);
46
+
47
+ if (!state) {
48
+ error("No brain state found. Run 'brain-dev init' first.");
49
+ return { error: 'no-state' };
50
+ }
51
+
52
+ // Handle --stop
53
+ if (values.stop) return handleStop(brainDir, state);
54
+
55
+ // Handle --dry-run
56
+ if (values['dry-run']) return handleDryRun(brainDir, state, values);
57
+
58
+ // Pre-flight checks
59
+ if (!state.project || !state.project.initialized) {
60
+ error("Project not initialized. Run '/brain:new-project' first.");
61
+ return { error: 'not-initialized' };
62
+ }
63
+
64
+ // Budget pre-flight: estimate a conservative cost for the auto run
65
+ const budgetCheck = canSpend(brainDir, 0);
66
+ if (!budgetCheck.allowed) {
67
+ error(`Budget exceeded: ${budgetCheck.reason}`);
68
+ return { error: 'budget-exceeded', reason: budgetCheck.reason, budget_status: budgetCheck.budget_status };
69
+ }
70
+
71
+ // Timeout pre-flight: check if already stuck
72
+ const timeoutCheck = checkTimeouts(brainDir, state);
73
+ if (timeoutCheck.tier === 'hard') {
74
+ error(`Hard timeout active: ${timeoutCheck.message}`);
75
+ return { error: 'hard-timeout', message: timeoutCheck.message };
76
+ }
77
+
78
+ // Acquire lock
79
+ const lockResult = acquireLock(brainDir, {
80
+ phase: state.phase.current,
81
+ operation: 'auto',
82
+ agent: 'orchestrator'
83
+ });
84
+ if (!lockResult.acquired) {
85
+ error('Another operation is in progress. Use /brain:recover if this is a stale lock.');
86
+ return { error: 'lock-held', previousLock: lockResult.previousLock };
87
+ }
88
+
89
+ // Update state for auto mode
90
+ state.auto.active = true;
91
+ state.auto.started_at = new Date().toISOString();
92
+ state.auto.current_step = 'starting';
93
+ state.auto.error_count = 0;
94
+ state.auto.phases_completed = 0;
95
+ writeState(brainDir, state);
96
+
97
+ logEvent(brainDir, state.phase.current, {
98
+ type: 'auto_start',
99
+ phase: state.phase.current
100
+ });
101
+
102
+ // Build runbook
103
+ const runbook = buildRunbook(brainDir, state, values);
104
+
105
+ // Build result
106
+ const result = {
107
+ action: 'auto-run',
108
+ ...runbook,
109
+ lockHeld: true,
110
+ instructions: buildInstructions(runbook, brainDir)
111
+ };
112
+
113
+ const humanLines = [
114
+ prefix('Auto mode started'),
115
+ prefix(`Phases: ${runbook.startPhase} → ${runbook.endPhase} (${runbook.phases.length} phases, ${runbook.totalSteps} steps)`),
116
+ prefix(`Guardrails: max ${runbook.config.max_errors} errors, ${runbook.config.max_operations} ops, ${runbook.config.max_duration_minutes}m`),
117
+ '',
118
+ result.instructions
119
+ ];
120
+ output(result, humanLines.join('\n'));
121
+
122
+ return result;
123
+ }
124
+
125
+ /**
126
+ * Build the runbook — a structured plan of phases and steps to execute.
127
+ * @param {string} brainDir
128
+ * @param {object} state
129
+ * @param {object} options - Parsed CLI values
130
+ * @returns {object} Runbook with phases, steps, and config
131
+ */
132
+ function buildRunbook(brainDir, state, options) {
133
+ const startPhase = options.phase ? parseInt(options.phase, 10) : state.phase.current;
134
+ const maxPhases = state.auto.max_phases || state.phase.total;
135
+ const endPhase = Math.min(startPhase + maxPhases - 1, state.phase.total);
136
+
137
+ const phases = [];
138
+ for (let p = startPhase; p <= endPhase; p++) {
139
+ const phaseInfo = Array.isArray(state.phase.phases)
140
+ ? state.phase.phases.find(ph => typeof ph === 'object' && ph.number === p)
141
+ : null;
142
+
143
+ if (!phaseInfo) continue;
144
+ if (phaseInfo.status === 'complete') continue;
145
+
146
+ phases.push({
147
+ number: p,
148
+ name: phaseInfo.name || `Phase ${p}`,
149
+ status: phaseInfo.status || 'pending',
150
+ steps: determineSteps(phaseInfo.status || 'pending', state)
151
+ });
152
+ }
153
+
154
+ return {
155
+ startPhase,
156
+ endPhase,
157
+ phases,
158
+ totalSteps: phases.reduce((sum, p) => sum + p.steps.length, 0),
159
+ config: {
160
+ max_errors: state.auto.max_errors || 3,
161
+ max_operations: state.auto.max_operations || 100,
162
+ max_duration_minutes: state.auto.max_duration_minutes || 120,
163
+ skip_discuss: state.auto.skip_discuss || false
164
+ }
165
+ };
166
+ }
167
+
168
+ /**
169
+ * Determine which steps remain for a phase based on its current status.
170
+ * The full pipeline is: discuss → plan → execute → verify → complete.
171
+ * @param {string} currentStatus - Phase status string
172
+ * @param {object} state - Brain state (for config flags)
173
+ * @returns {string[]} Remaining step names
174
+ */
175
+ function determineSteps(currentStatus, state) {
176
+ const allSteps = ['discuss', 'plan', 'execute', 'verify', 'complete'];
177
+ const statusToStep = {
178
+ 'initialized': 0,
179
+ 'pending': 0,
180
+ 'discussing': 0,
181
+ 'discussed': 1,
182
+ 'planning': 1,
183
+ 'planned': 2,
184
+ 'executing': 2,
185
+ 'executed': 3,
186
+ 'verifying': 3,
187
+ 'verified': 4,
188
+ 'complete': 5
189
+ };
190
+
191
+ const startIdx = statusToStep[currentStatus] || 0;
192
+ let steps = allSteps.slice(startIdx);
193
+
194
+ // Skip discuss if configured or if auto mode has skip_discuss
195
+ if (state.auto && state.auto.skip_discuss && steps[0] === 'discuss') {
196
+ steps = steps.slice(1);
197
+ }
198
+
199
+ return steps;
200
+ }
201
+
202
+ /**
203
+ * Build human-readable instructions for Claude Code to follow.
204
+ * This is the core output — a markdown runbook that tells the agent
205
+ * exactly which commands to run in sequence.
206
+ * @param {object} runbook - Built runbook
207
+ * @param {string} brainDir - Path to .brain/
208
+ * @returns {string} Markdown instruction text
209
+ */
210
+ function buildInstructions(runbook, brainDir) {
211
+ const lines = [];
212
+ lines.push(`## Auto Mode — ${runbook.phases.length} phase${runbook.phases.length !== 1 ? 's' : ''}, ${runbook.totalSteps} steps`);
213
+ lines.push(`Guardrails: max ${runbook.config.max_errors} errors, ${runbook.config.max_operations} operations, ${runbook.config.max_duration_minutes}m duration`);
214
+ lines.push('');
215
+
216
+ for (const phase of runbook.phases) {
217
+ lines.push(`### Phase ${phase.number}: ${phase.name}`);
218
+ lines.push(`Current status: ${phase.status}`);
219
+ lines.push('');
220
+
221
+ let stepNum = 1;
222
+ for (const step of phase.steps) {
223
+ switch (step) {
224
+ case 'discuss':
225
+ lines.push(`${stepNum}. Run: \`npx brain-dev discuss --phase ${phase.number}\` → follow the discuss agent prompt`);
226
+ break;
227
+ case 'plan':
228
+ lines.push(`${stepNum}. Run: \`npx brain-dev plan --phase ${phase.number}\` → follow the planner agent prompt, then plan-checker loop`);
229
+ break;
230
+ case 'execute':
231
+ lines.push(`${stepNum}. Run: \`npx brain-dev execute --phase ${phase.number}\` → repeat until all plans executed`);
232
+ break;
233
+ case 'verify':
234
+ lines.push(`${stepNum}. Run: \`npx brain-dev verify --phase ${phase.number}\` → follow the verifier agent prompt`);
235
+ break;
236
+ case 'complete':
237
+ lines.push(`${stepNum}. Run: \`npx brain-dev complete --phase ${phase.number}\` → advance to next phase`);
238
+ break;
239
+ }
240
+ stepNum++;
241
+ }
242
+ lines.push('');
243
+ }
244
+
245
+ lines.push('### Error Handling');
246
+ lines.push('- If any step fails, retry once. If it fails again, run: `npx brain-dev auto --stop`');
247
+ lines.push('- Budget check: run `npx brain-dev progress --cost` between phases');
248
+ lines.push('- Timeout check: run `npx brain-dev progress --stuck` if a step takes too long');
249
+ lines.push('');
250
+ lines.push('### Completion');
251
+ lines.push('- When all phases complete or auto stops, run: `npx brain-dev auto --stop` to release the lock');
252
+
253
+ return lines.join('\n');
254
+ }
255
+
256
+ /**
257
+ * Handle --stop: deactivate auto mode, release lock, record history.
258
+ * @param {string} brainDir
259
+ * @param {object} state
260
+ * @returns {object} Result
261
+ */
262
+ function handleStop(brainDir, state) {
263
+ const wasActive = state.auto.active;
264
+
265
+ state.auto.active = false;
266
+ state.auto.current_step = 'stopped';
267
+
268
+ // Record history entry
269
+ if (state.auto.started_at) {
270
+ if (!Array.isArray(state.auto.history)) {
271
+ state.auto.history = [];
272
+ }
273
+ state.auto.history.push({
274
+ started: state.auto.started_at,
275
+ ended: new Date().toISOString(),
276
+ phases_completed: state.auto.phases_completed || 0,
277
+ result: 'stopped',
278
+ errors: state.auto.error_count || 0
279
+ });
280
+ }
281
+
282
+ writeState(brainDir, state);
283
+ releaseLock(brainDir);
284
+
285
+ logEvent(brainDir, state.phase.current, {
286
+ type: 'auto_stop',
287
+ phases_completed: state.auto.phases_completed || 0
288
+ });
289
+
290
+ const result = {
291
+ action: 'auto-stopped',
292
+ phases_completed: state.auto.phases_completed || 0,
293
+ was_active: wasActive
294
+ };
295
+
296
+ const msg = wasActive
297
+ ? `Auto mode stopped. ${result.phases_completed} phase(s) completed.`
298
+ : 'Auto mode was not active. Lock released.';
299
+ output(result, prefix(msg));
300
+
301
+ return result;
302
+ }
303
+
304
+ /**
305
+ * Handle --dry-run: build and display the runbook without modifying state.
306
+ * @param {string} brainDir
307
+ * @param {object} state
308
+ * @param {object} options - Parsed CLI values
309
+ * @returns {object} Result
310
+ */
311
+ function handleDryRun(brainDir, state, options) {
312
+ if (!state.project || !state.project.initialized) {
313
+ error("Project not initialized. Run '/brain:new-project' first.");
314
+ return { error: 'not-initialized' };
315
+ }
316
+
317
+ const runbook = buildRunbook(brainDir, state, options);
318
+ const instructions = buildInstructions(runbook, brainDir);
319
+
320
+ const result = {
321
+ action: 'auto-dry-run',
322
+ ...runbook,
323
+ instructions
324
+ };
325
+
326
+ const humanLines = [
327
+ prefix('Auto mode dry run (no state changes)'),
328
+ prefix(`Phases: ${runbook.startPhase} → ${runbook.endPhase} (${runbook.phases.length} phases, ${runbook.totalSteps} steps)`),
329
+ '',
330
+ instructions
331
+ ];
332
+ output(result, humanLines.join('\n'));
333
+
334
+ return result;
335
+ }
336
+
337
+ module.exports = { run };
@@ -0,0 +1,177 @@
1
+ 'use strict';
2
+
3
+ const { parseArgs } = require('node:util');
4
+ const path = require('node:path');
5
+ const fs = require('node:fs');
6
+ const { output, error, success, prefix } = require('../core.cjs');
7
+ const { readState } = require('../state.cjs');
8
+ const { collectState } = require('../dashboard-collector.cjs');
9
+
10
+ /**
11
+ * brain-dev dashboard command.
12
+ * Modes: TUI (default), --browser (deferred), --json, --stop, --status.
13
+ * @param {string[]} args - CLI arguments
14
+ * @param {object} [opts] - Options (brainDir override)
15
+ * @returns {object} Action result
16
+ */
17
+ async function run(args = [], opts = {}) {
18
+ const { values } = parseArgs({
19
+ args,
20
+ options: {
21
+ browser: { type: 'boolean', default: false },
22
+ stop: { type: 'boolean', default: false },
23
+ status: { type: 'boolean', default: false },
24
+ port: { type: 'string' },
25
+ json: { type: 'boolean', default: false }
26
+ },
27
+ strict: false
28
+ });
29
+
30
+ const brainDir = opts.brainDir || path.join(process.cwd(), '.brain');
31
+ const state = readState(brainDir);
32
+
33
+ if (values.stop) return handleStop(brainDir);
34
+ if (values.status) return handleStatus(brainDir);
35
+
36
+ if (values.browser) {
37
+ // Web dashboard — defer to server module
38
+ return { action: 'dashboard-browser', message: 'Web dashboard coming in next iteration. Use TUI mode (default).' };
39
+ }
40
+
41
+ // Default: TUI mode
42
+ return renderTUI(brainDir, state, values);
43
+ }
44
+
45
+ /**
46
+ * Render a text-based dashboard to stdout.
47
+ * @param {string} brainDir
48
+ * @param {object} state
49
+ * @param {object} options - Parsed CLI flags
50
+ * @returns {object} Action result with snapshot data
51
+ */
52
+ function renderTUI(brainDir, state, options) {
53
+ const snapshot = collectState(brainDir);
54
+
55
+ if (options.json) {
56
+ return output(snapshot);
57
+ }
58
+
59
+ const lines = [];
60
+ const { styleText } = require('node:util');
61
+
62
+ lines.push(prefix('') + styleText('bold', ' Dashboard') + ' ' + new Date().toLocaleString());
63
+ lines.push('\u2500'.repeat(72));
64
+
65
+ // Project info
66
+ lines.push(`Project: ${styleText('bold', snapshot.project.name)} (${snapshot.project.mode}, ${snapshot.project.depth})` +
67
+ (snapshot.milestone?.current ? ` Milestone: ${snapshot.milestone.current}` : ''));
68
+ lines.push('');
69
+
70
+ // Phase progress
71
+ lines.push(`Phase Progress [${snapshot.phases.current}/${snapshot.phases.total}]`);
72
+ for (const phase of snapshot.phases.list) {
73
+ const isCurrent = phase.number === snapshot.phases.current;
74
+ const bar = renderProgressBar(phase.status);
75
+ const marker = isCurrent ? ' <--' : '';
76
+ const statusColor = phase.status === 'complete' ? 'green' : (isCurrent ? 'yellow' : 'dim');
77
+ lines.push(` ${String(phase.number).padStart(2, ' ')}. ${phase.name.padEnd(16)} ${bar} ${styleText(statusColor, phase.status)}${marker}`);
78
+ }
79
+ lines.push('');
80
+
81
+ // Timing
82
+ if (snapshot.timing.phaseElapsed > 0) {
83
+ const elapsed = formatDuration(snapshot.timing.phaseElapsed);
84
+ lines.push(`Timing: ${elapsed} elapsed`);
85
+ }
86
+
87
+ // Context + Cost
88
+ const contextStr = snapshot.context.remainingPct !== null
89
+ ? `Context: ${snapshot.context.remainingPct}% (${snapshot.context.level})`
90
+ : 'Context: unknown';
91
+ let costStr = '';
92
+ if (snapshot.cost.spent > 0) {
93
+ costStr = ` | Cost: $${snapshot.cost.spent.toFixed(2)}`;
94
+ if (snapshot.cost.ceiling) costStr += `/$${snapshot.cost.ceiling}`;
95
+ costStr += ` (${snapshot.cost.status})`;
96
+ }
97
+ lines.push(contextStr + costStr);
98
+ lines.push('');
99
+
100
+ // Recent events
101
+ if (snapshot.events.length > 0) {
102
+ lines.push('Recent Events:');
103
+ const recent = snapshot.events.slice(-5);
104
+ for (const evt of recent) {
105
+ const time = evt.timestamp ? new Date(evt.timestamp).toLocaleTimeString().slice(0, 5) : '??:??';
106
+ lines.push(` ${time} ${(evt.type || '?').padEnd(12)} ${evt.plan ? 'plan-' + evt.plan : ''} ${evt.agent || ''}`);
107
+ }
108
+ lines.push('');
109
+ }
110
+
111
+ // Health / Stuck / Lock
112
+ const healthStr = snapshot.health.stateValid ? 'OK' : 'ERROR';
113
+ const stuckStr = snapshot.stuck.detected ? styleText('red', `YES (${snapshot.stuck.tier})`) : 'No';
114
+ const lockStr = snapshot.lock.active ? (snapshot.lock.stale ? styleText('red', 'stale') : 'active (auto)') : 'none';
115
+ lines.push(`Health: ${healthStr} | Stuck: ${stuckStr} | Lock: ${lockStr}`);
116
+
117
+ const text = lines.join('\n');
118
+ console.log(text);
119
+ return { action: 'dashboard-tui', data: snapshot };
120
+ }
121
+
122
+ /**
123
+ * Render an ASCII progress bar based on phase status.
124
+ * @param {string} status
125
+ * @returns {string} 22-char progress bar like [==== ]
126
+ */
127
+ function renderProgressBar(status) {
128
+ const pct = status === 'complete' ? 100 :
129
+ status === 'verified' ? 90 :
130
+ status === 'executing' ? 55 :
131
+ status === 'planning' ? 25 :
132
+ status === 'discussing' ? 10 : 0;
133
+ const filled = Math.round(pct / 5);
134
+ const empty = 20 - filled;
135
+ return '[' + '='.repeat(filled) + ' '.repeat(empty) + ']';
136
+ }
137
+
138
+ /**
139
+ * Format seconds into human-readable "Xm Ys" string.
140
+ * @param {number} seconds
141
+ * @returns {string}
142
+ */
143
+ function formatDuration(seconds) {
144
+ const m = Math.floor(seconds / 60);
145
+ const s = seconds % 60;
146
+ return `${m}m ${s}s`;
147
+ }
148
+
149
+ /**
150
+ * Handle --stop: remove dashboard server port file.
151
+ * @param {string} brainDir
152
+ * @returns {object}
153
+ */
154
+ function handleStop(brainDir) {
155
+ const portFile = path.join(brainDir, '.dashboard-port');
156
+ if (!fs.existsSync(portFile)) {
157
+ return { action: 'dashboard-not-running', message: 'No dashboard server running.' };
158
+ }
159
+ try { fs.unlinkSync(portFile); } catch {}
160
+ return { action: 'dashboard-stopped' };
161
+ }
162
+
163
+ /**
164
+ * Handle --status: check if a dashboard server is running.
165
+ * @param {string} brainDir
166
+ * @returns {object}
167
+ */
168
+ function handleStatus(brainDir) {
169
+ const portFile = path.join(brainDir, '.dashboard-port');
170
+ if (fs.existsSync(portFile)) {
171
+ const port = fs.readFileSync(portFile, 'utf8').trim();
172
+ return { action: 'dashboard-status', running: true, url: `http://localhost:${port}` };
173
+ }
174
+ return { action: 'dashboard-status', running: false };
175
+ }
176
+
177
+ module.exports = { run };
@@ -7,6 +7,8 @@ const { loadTemplate, interpolate } = require('../templates.cjs');
7
7
  const { getAgent, resolveModel } = require('../agents.cjs');
8
8
  const { logEvent } = require('../logger.cjs');
9
9
  const { output, error } = require('../core.cjs');
10
+ const { recordInvocation, estimateTokens } = require('../cost.cjs');
11
+ const { acquireLock, releaseLock } = require('../lock.cjs');
10
12
 
11
13
  /**
12
14
  * Find a phase directory under .brain/phases/ matching a phase number.
@@ -366,8 +368,24 @@ async function run(args = [], opts = {}) {
366
368
  `> ${debuggerSpawnInstruction}`
367
369
  ].join('\n');
368
370
 
371
+ // Record cost estimate
372
+ try {
373
+ const inputTokens = estimateTokens(fullPrompt || prompt || '');
374
+ recordInvocation(brainDir, {
375
+ agent: 'executor',
376
+ phase: phaseNumber,
377
+ plan: padded,
378
+ model: model || 'inherit',
379
+ input_tokens: inputTokens,
380
+ output_tokens: 0 // Will be updated when SUMMARY is written
381
+ });
382
+ } catch { /* cost tracking failure is non-fatal */ }
383
+
369
384
  // Update state to executing
370
385
  state.phase.status = 'executing';
386
+ if (!state.phase.execution_started_at) {
387
+ state.phase.execution_started_at = new Date().toISOString();
388
+ }
371
389
  writeState(brainDir, state);
372
390
 
373
391
  // Check auto-recover config
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const path = require('node:path');
4
+ const { parseArgs } = require('node:util');
4
5
  const { readState } = require('../state.cjs');
5
6
  const { output, prefix, error } = require('../core.cjs');
6
7
 
@@ -58,6 +59,17 @@ function nextAction(state) {
58
59
  * @returns {object} Structured result for piping/testing
59
60
  */
60
61
  async function run(args = [], opts = {}) {
62
+ const { values } = parseArgs({
63
+ args,
64
+ options: {
65
+ verbose: { type: 'boolean', default: false },
66
+ cost: { type: 'boolean', default: false },
67
+ breakdown: { type: 'string' },
68
+ json: { type: 'boolean', default: false }
69
+ },
70
+ strict: false
71
+ });
72
+
61
73
  const brainDir = opts.brainDir || path.join(process.cwd(), '.brain');
62
74
  const state = readState(brainDir);
63
75
 
@@ -66,7 +78,7 @@ async function run(args = [], opts = {}) {
66
78
  return { error: 'no-state' };
67
79
  }
68
80
 
69
- const verbose = args.includes('--verbose');
81
+ const verbose = values.verbose || args.includes('--verbose');
70
82
  const phase = state.phase || {};
71
83
  const phases = phase.phases || [];
72
84
  const session = state.session || {};
@@ -160,6 +172,30 @@ async function run(args = [], opts = {}) {
160
172
  contextWarning
161
173
  };
162
174
 
175
+ // Cost display (--cost flag)
176
+ if (values.cost) {
177
+ try {
178
+ const { readMetrics, checkBudget, getAgentBreakdown, getPhaseBreakdown } = require('../cost.cjs');
179
+ const metrics = readMetrics(brainDir);
180
+ const budgetStatus = checkBudget(metrics);
181
+ result.cost = {
182
+ spent: metrics.totals?.estimated_cost_usd || 0,
183
+ ceiling: state.budget?.ceiling_usd || null,
184
+ budget_status: budgetStatus.status,
185
+ invocations: metrics.totals?.agent_invocations || 0
186
+ };
187
+ if (values.breakdown === 'agent') {
188
+ result.cost.breakdown = getAgentBreakdown(metrics);
189
+ } else if (values.breakdown === 'phase') {
190
+ result.cost.breakdown = getPhaseBreakdown(metrics);
191
+ }
192
+ let costLine = prefix(`Cost: $${result.cost.spent.toFixed(2)}`);
193
+ if (result.cost.ceiling) costLine += ` / $${result.cost.ceiling} (${budgetStatus.status})`;
194
+ costLine += ` | ${result.cost.invocations} invocations`;
195
+ lines.push(costLine);
196
+ } catch { /* cost not available */ }
197
+ }
198
+
163
199
  output(result, lines.join('\n'));
164
200
  return result;
165
201
  }