dual-brain 4.2.0 → 4.5.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/install.mjs CHANGED
@@ -8,7 +8,7 @@
8
8
  * npx dual-brain --dry-run # detect only, don't install
9
9
  * npx dual-brain --help
10
10
  */
11
- import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, unlinkSync, writeFileSync } from 'fs';
11
+ import { appendFileSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync } from 'fs';
12
12
  import { dirname, join, resolve } from 'path';
13
13
  import { fileURLToPath } from 'url';
14
14
  import { spawnSync } from 'child_process';
@@ -43,38 +43,95 @@ if (flag('--help') || flag('-h')) {
43
43
 
44
44
  Usage: npx -y dual-brain [command] [options]
45
45
 
46
- ⌨️ Commands:
47
- (none) 🧠 Auto-detect and install/update orchestrator
48
- status 🟢 Open live control panel
49
- mode 🎛️ Show or switch profile
50
- budget 💵 Set session/daily spend limits
51
- explain 🧭 Explain last routing decision
46
+ Setup:
47
+ (none) Auto-detect and install/update orchestrator
52
48
  init Alias for default install
49
+ doctor Check system health and report issues
50
+ reset Clear all state files (keeps config/hooks)
51
+ repair Fix corrupt files, stale locks, re-register hooks
52
+ --uninstall Remove dual-brain hooks and state files
53
+
54
+ Status:
55
+ status Open live control panel
56
+ health Verify system health
57
+ budget Show or set session/daily spend limits
58
+ cost Activity and cost estimates
59
+ report Generate session report
60
+
61
+ Routing:
62
+ mode Show or switch profile
63
+ explain Explain last routing decision
64
+ gate Run quality gate
65
+ ledger Routing outcome insights
66
+
67
+ GPT:
68
+ think Dual-brain think (--question "..." [--round 2])
69
+ review Dual-brain code review
70
+ dispatch Dispatch work to GPT via Codex CLI
71
+
72
+ Vibe:
73
+ vibe Decompose casual requests into structured work
74
+ plan Generate execution plans (--utterance "...")
75
+ memory Persistent preferences and work threads
76
+
77
+ Agents:
78
+ agent Run a specialist agent template (explorer, security-review, test-writer, bug-hunter)
79
+ agents List all available agent templates
80
+ chain Run a built-in agent chain (explore-then-fix, review-and-test, audit-and-plan)
81
+ chains List all available agent chains
82
+
83
+ Ship Captain:
84
+ do "<goal>" Run full pipeline: agents → tests → gate → PR
85
+ --yolo Skip all confirmations
86
+ --careful Confirm every step
87
+ --plan-only Show plan without executing
88
+ --no-pr Skip PR creation
89
+ ship Create branch, run tests, open PR
90
+ test-run Discover and run project tests
91
+ diff Show current changes summary
92
+ runs List recent Ship Captain runs
93
+ resume Resume last incomplete run
94
+
95
+ Dev:
96
+ test Run self-tests
53
97
 
54
98
  Options:
55
99
  --force Overwrite all existing config
56
100
  --dry-run Detect environment only
57
101
  --json Output detection as JSON
58
- --uninstall Remove dual-brain hooks and state files
59
102
  --help Show this help
60
103
 
61
- 🎛️ Routing modes:
62
- 🤖 Auto (default) Adapts routing based on risk, health, outcomes
63
- ⚖️ Balanced Auto-routes, uses both providers evenly
64
- 🛡️ Conservative Fewer GPT dispatches, sticks to Claude
65
- 🚀 Aggressive Maximizes both subscriptions, dual-brain for medium+
104
+ Routing modes:
105
+ Auto (default) Adapts routing based on risk, health, outcomes
106
+ Balanced Auto-routes, uses both providers evenly
107
+ Conservative Fewer GPT dispatches, sticks to Claude
108
+ Aggressive Maximizes both subscriptions, dual-brain for medium+
66
109
 
67
- 🚀 Examples:
110
+ Examples:
68
111
  ${cmd('npx dual-brain')} # install or update
69
112
  ${cmd('npx dual-brain status')} # open control panel
70
113
  ${cmd('npx dual-brain mode cost-saver')} # switch profile
71
114
  ${cmd('npx dual-brain budget 8 25')} # \$8 session / \$25 daily
72
- ${cmd('npx dual-brain explain')} # last routing decision
115
+ ${cmd('npx dual-brain think --question "should we use Redis?"')}
116
+ ${cmd('npx dual-brain vibe "fix login and update nav"')}
117
+ ${cmd('npx dual-brain agents')} # list agent templates
118
+ ${cmd('npx dual-brain agent explorer --question "where is auth handled?"')}
119
+ ${cmd('npx dual-brain agent security-review --scope src/auth --severity high')}
120
+ ${cmd('npx dual-brain chains')} # list available chains
121
+ ${cmd('npx dual-brain chain explore-then-fix --question "auth bug" --scope "src/auth"')}
73
122
  `);
74
123
  process.exit(0);
75
124
  }
76
125
 
77
- const SUBCOMMANDS = ['init', 'status', 'mode', 'budget', 'explain'];
126
+ const SUBCOMMANDS = [
127
+ 'init', 'status', 'mode', 'budget', 'explain',
128
+ 'review', 'think', 'health', 'report', 'gate',
129
+ 'vibe', 'plan', 'cost', 'dispatch', 'memory',
130
+ 'test', 'ledger', 'doctor', 'reset', 'repair',
131
+ 'chain', 'chains',
132
+ 'agent', 'agents',
133
+ 'do', 'ship', 'test-run', 'diff', 'runs', 'resume',
134
+ ];
78
135
  if (subcommand && !SUBCOMMANDS.includes(subcommand)) {
79
136
  console.error(` Unknown command: ${subcommand}`);
80
137
  console.error(` Run: ${cmd('npx dual-brain --help')}`);
@@ -268,7 +325,7 @@ function generateSettings(workspace) {
268
325
  ],
269
326
  PostToolUse: [
270
327
  {
271
- matcher: '',
328
+ matcher: 'Agent|Bash|Write|Edit',
272
329
  hooks: [{ type: 'command', command: 'node .claude/hooks/cost-logger.mjs' }],
273
330
  },
274
331
  ],
@@ -341,6 +398,8 @@ function install(workspace, env, mode) {
341
398
  'summary-checkpoint.mjs', 'decision-ledger.mjs', 'control-panel.mjs',
342
399
  'risk-classifier.mjs', 'failure-detector.mjs',
343
400
  'vibe-router.mjs', 'plan-generator.mjs', 'vibe-memory.mjs',
401
+ 'agent-templates.mjs', 'agent-chains.mjs',
402
+ 'ship-captain.mjs', 'ship-gate.mjs', 'confirmation-policy.mjs',
344
403
  ];
345
404
  for (const h of HOOKS) cpSync(join(__dirname, 'hooks', h), join(target, 'hooks', h));
346
405
  actions.push(`✓ ${HOOKS.length} hook scripts`);
@@ -385,6 +444,134 @@ function install(workspace, env, mode) {
385
444
  return actions;
386
445
  }
387
446
 
447
+ // ─── Guided Auth ────────────────────────────────────────────────────────────
448
+
449
+ function printGuidedAuth(env) {
450
+ const claudeOk = env.claude.installed && env.claude.authed;
451
+ const codexOk = env.codex.installed && env.codex.authed;
452
+
453
+ if (claudeOk && codexOk) return false; // nothing to guide
454
+
455
+ const claudeMissing = !env.claude.installed || !env.claude.authed;
456
+ const codexMissing = !env.codex.installed || !env.codex.authed;
457
+
458
+ console.log('');
459
+
460
+ if (claudeMissing && codexMissing) {
461
+ console.log(' ⚠️ No AI providers detected. You need at least one:');
462
+ console.log('');
463
+ console.log(' Claude (recommended):');
464
+ console.log(' npm install -g @anthropic-ai/claude-code && claude login');
465
+ console.log('');
466
+ console.log(' OpenAI (optional, enables dual-brain):');
467
+ console.log(' npm install -g @openai/codex && codex login');
468
+ console.log('');
469
+ console.log(' Then re-run: npx dual-brain');
470
+ console.log('');
471
+ return true;
472
+ }
473
+
474
+ if (claudeMissing) {
475
+ if (!env.claude.installed) {
476
+ console.log(' ⚠️ Claude CLI not detected. To enable Claude routing:');
477
+ } else {
478
+ console.log(' ⚠️ Claude CLI not authenticated. To enable Claude routing:');
479
+ }
480
+ console.log('');
481
+ console.log(' 1. Install: npm install -g @anthropic-ai/claude-code');
482
+ console.log(' 2. Login: claude login');
483
+ console.log('');
484
+ console.log(' Run these commands, then re-run: npx dual-brain');
485
+ console.log('');
486
+ }
487
+
488
+ if (codexMissing) {
489
+ if (!env.codex.installed) {
490
+ console.log(' ℹ️ Codex CLI not detected. To enable GPT routing:');
491
+ } else {
492
+ console.log(' ℹ️ Codex CLI not authenticated. To enable GPT routing:');
493
+ }
494
+ console.log('');
495
+ console.log(' 1. Install: npm install -g @openai/codex');
496
+ console.log(' 2. Login: codex login');
497
+ console.log('');
498
+ if (claudeMissing) {
499
+ console.log(' Run these commands, then re-run: npx dual-brain');
500
+ } else {
501
+ console.log(' Run these commands, then re-run: npx dual-brain');
502
+ console.log(' GPT features will be disabled until Codex is configured.');
503
+ }
504
+ console.log('');
505
+ }
506
+
507
+ return claudeMissing; // only block/poll if Claude (primary provider) is missing
508
+ }
509
+
510
+ async function waitForAuth(env) {
511
+ if (!process.stdin.isTTY || !process.stdout.isTTY) return;
512
+
513
+ const claudeMissing = !env.claude.installed || !env.claude.authed;
514
+ if (!claudeMissing) return; // only wait if primary provider needs setup
515
+
516
+ process.stdout.write(' Would you like me to wait while you authenticate? [Y/n] ');
517
+
518
+ const answer = await new Promise((resolve) => {
519
+ process.stdin.setEncoding('utf8');
520
+ process.stdin.once('data', (chunk) => resolve(chunk.trim().toLowerCase()));
521
+ process.stdin.resume();
522
+ });
523
+
524
+ if (answer === 'n' || answer === 'no') {
525
+ process.stdin.pause();
526
+ console.log('');
527
+ console.log(' Re-run `npx dual-brain` after authenticating.');
528
+ console.log('');
529
+ process.exit(0);
530
+ }
531
+
532
+ console.log('');
533
+ console.log(' Waiting for Claude CLI to become available (checking every 5s, max 5 min)...');
534
+ console.log('');
535
+
536
+ const maxAttempts = 60; // 60 × 5s = 5 minutes
537
+ for (let i = 0; i < maxAttempts; i++) {
538
+ await new Promise(r => setTimeout(r, 5000));
539
+ process.stdout.write('.');
540
+ const fresh = detectClaude();
541
+ if (fresh.installed) {
542
+ console.log('');
543
+ console.log('');
544
+ console.log(' Claude CLI detected! Continuing setup...');
545
+ console.log('');
546
+ process.stdin.pause();
547
+ // Update env in-place
548
+ env.claude = fresh;
549
+ return;
550
+ }
551
+ }
552
+
553
+ console.log('');
554
+ console.log('');
555
+ console.log(' Timed out after 5 minutes. Re-run `npx dual-brain` after authenticating.');
556
+ console.log('');
557
+ process.stdin.pause();
558
+ process.exit(1);
559
+ }
560
+
561
+ // ─── Quick Start Block ───────────────────────────────────────────────────────
562
+
563
+ function printQuickStart() {
564
+ console.log(' ✓ dual-brain installed successfully');
565
+ console.log('');
566
+ console.log(' Quick start:');
567
+ console.log(` npx dual-brain do "fix a bug and write tests" ← full pipeline`);
568
+ console.log(' npx dual-brain status ← check system');
569
+ console.log(' npx dual-brain doctor ← diagnose issues');
570
+ console.log('');
571
+ console.log(' All commands: npx dual-brain --help');
572
+ console.log('');
573
+ }
574
+
388
575
  // ─── Status Report ──────────────────────────────────────────────────────────
389
576
 
390
577
  function printReport(env, mode, actions, isDryRun) {
@@ -405,8 +592,6 @@ function printReport(env, mode, actions, isDryRun) {
405
592
  if (actions) {
406
593
  lines.push(sep());
407
594
  for (const a of actions) lines.push(ln(` ${a}`));
408
- lines.push(sep());
409
- lines.push(ln('✅ Installed — launching session manager...'));
410
595
  } else if (isDryRun) {
411
596
  lines.push(sep());
412
597
  lines.push(ln('Dry run — no files written'));
@@ -698,6 +883,431 @@ function cmdExplain() {
698
883
  console.log('');
699
884
  }
700
885
 
886
+ // ─── Subcommand: doctor ───────────────────────────────────────────────────
887
+
888
+ function cmdDoctor() {
889
+ const workspace = resolve(process.cwd());
890
+ const claudeDir = join(workspace, '.claude');
891
+ const hooksDir = join(claudeDir, 'hooks');
892
+ const results = [];
893
+
894
+ // 1. Hook installation check
895
+ const settingsPath = join(claudeDir, 'settings.json');
896
+ const DUAL_BRAIN_CMDS = [
897
+ 'node .claude/hooks/enforce-tier.mjs',
898
+ 'node .claude/hooks/cost-logger.mjs',
899
+ ];
900
+ if (existsSync(settingsPath)) {
901
+ try {
902
+ const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
903
+ const registeredCmds = [];
904
+ if (settings.hooks) {
905
+ for (const entries of Object.values(settings.hooks)) {
906
+ for (const entry of entries) {
907
+ for (const h of (entry.hooks || [])) {
908
+ if (DUAL_BRAIN_CMDS.includes(h.command)) registeredCmds.push(h.command);
909
+ }
910
+ }
911
+ }
912
+ }
913
+ const missing = DUAL_BRAIN_CMDS.filter(c => !registeredCmds.includes(c));
914
+ if (missing.length === 0) {
915
+ results.push({ name: 'Hook registration', status: 'pass', detail: `${DUAL_BRAIN_CMDS.length}/${DUAL_BRAIN_CMDS.length} hooks registered` });
916
+ } else {
917
+ results.push({ name: 'Hook registration', status: 'fail', detail: `Missing: ${missing.map(c => c.split('/').pop()).join(', ')}`, fix: 'Run: npx dual-brain repair' });
918
+ }
919
+ } catch (err) {
920
+ results.push({ name: 'Hook registration', status: 'fail', detail: `settings.json parse error: ${err.message}`, fix: 'Run: npx dual-brain repair' });
921
+ }
922
+ } else {
923
+ results.push({ name: 'Hook registration', status: 'fail', detail: 'settings.json not found', fix: 'Run: npx dual-brain' });
924
+ }
925
+
926
+ // 2. Config validity
927
+ const orchPath = join(claudeDir, 'orchestrator.json');
928
+ if (existsSync(orchPath)) {
929
+ try {
930
+ const config = JSON.parse(readFileSync(orchPath, 'utf8'));
931
+ const requiredKeys = ['subscriptions', 'tiers', 'providers', 'budgets'];
932
+ const missing = requiredKeys.filter(k => !config[k]);
933
+ if (missing.length === 0) {
934
+ results.push({ name: 'Config validity', status: 'pass', detail: 'orchestrator.json valid, all required keys present' });
935
+ } else {
936
+ results.push({ name: 'Config validity', status: 'warn', detail: `Missing keys: ${missing.join(', ')}`, fix: 'Run: npx dual-brain --force' });
937
+ }
938
+ } catch (err) {
939
+ results.push({ name: 'Config validity', status: 'fail', detail: `orchestrator.json corrupt: ${err.message}`, fix: 'Run: npx dual-brain repair' });
940
+ }
941
+ } else {
942
+ results.push({ name: 'Config validity', status: 'fail', detail: 'orchestrator.json not found', fix: 'Run: npx dual-brain' });
943
+ }
944
+
945
+ // 3. Auth status
946
+ const claude = detectClaude();
947
+ if (claude.authed) {
948
+ results.push({ name: 'Claude CLI', status: 'pass', detail: 'installed and authenticated' });
949
+ } else if (claude.installed) {
950
+ results.push({ name: 'Claude CLI', status: 'warn', detail: 'installed but not authenticated' });
951
+ } else {
952
+ results.push({ name: 'Claude CLI', status: 'warn', detail: 'not found' });
953
+ }
954
+
955
+ const codexResult = detectCodex();
956
+ if (codexResult.authed) {
957
+ results.push({ name: 'Codex CLI', status: 'pass', detail: 'installed and authenticated' });
958
+ } else if (codexResult.installed) {
959
+ results.push({ name: 'Codex CLI', status: 'warn', detail: 'installed but not authenticated' });
960
+ } else {
961
+ results.push({ name: 'Codex CLI', status: 'warn', detail: 'not found (GPT features disabled)' });
962
+ }
963
+
964
+ // 4. State file health
965
+ const stateCheckFiles = [
966
+ { path: join(hooksDir, '.burst-state'), label: '.burst-state', format: 'json' },
967
+ { path: join(hooksDir, 'burst-state.json'), label: 'burst-state.json', format: 'json' },
968
+ { path: join(hooksDir, '.drift-warned'), label: '.drift-warned', format: 'any' },
969
+ { path: join(claudeDir, 'dual-brain.profile.json'), label: 'dual-brain.profile.json', format: 'json' },
970
+ { path: join(hooksDir, 'decision-ledger.jsonl'), label: 'decision-ledger.jsonl', format: 'jsonl' },
971
+ ];
972
+
973
+ // Add any usage-*.jsonl files
974
+ try {
975
+ for (const f of readdirSync(hooksDir)) {
976
+ if (f.startsWith('usage-') && f.endsWith('.jsonl')) {
977
+ stateCheckFiles.push({ path: join(hooksDir, f), label: f, format: 'jsonl' });
978
+ }
979
+ }
980
+ } catch {}
981
+
982
+ let stateHealthy = 0, stateWarns = 0, stateFails = 0;
983
+ const stateIssues = [];
984
+
985
+ for (const sf of stateCheckFiles) {
986
+ if (!existsSync(sf.path)) continue;
987
+ try {
988
+ const raw = readFileSync(sf.path, 'utf8');
989
+ if (sf.format === 'json') {
990
+ JSON.parse(raw);
991
+ stateHealthy++;
992
+ } else if (sf.format === 'jsonl') {
993
+ const lines = raw.split('\n').filter(Boolean);
994
+ let badLines = 0;
995
+ for (const line of lines) {
996
+ try { JSON.parse(line); } catch { badLines++; }
997
+ }
998
+ if (badLines > 0) {
999
+ stateWarns++;
1000
+ stateIssues.push(`${sf.label}: ${badLines}/${lines.length} unparseable lines`);
1001
+ } else {
1002
+ stateHealthy++;
1003
+ }
1004
+ } else {
1005
+ stateHealthy++;
1006
+ }
1007
+ } catch (err) {
1008
+ stateFails++;
1009
+ stateIssues.push(`${sf.label}: corrupt (${err.message})`);
1010
+ }
1011
+ }
1012
+
1013
+ // Check for stale locks
1014
+ let staleLocks = 0;
1015
+ for (const dir of [hooksDir, claudeDir]) {
1016
+ try {
1017
+ for (const f of readdirSync(dir)) {
1018
+ if (f.endsWith('.lock')) {
1019
+ try {
1020
+ const st = statSync(join(dir, f));
1021
+ if (Date.now() - st.mtimeMs > 30_000) staleLocks++;
1022
+ } catch {}
1023
+ }
1024
+ }
1025
+ } catch {}
1026
+ }
1027
+
1028
+ if (staleLocks > 0) {
1029
+ stateIssues.push(`${staleLocks} stale lock file(s)`);
1030
+ stateWarns++;
1031
+ }
1032
+
1033
+ if (stateFails > 0) {
1034
+ results.push({ name: 'State files', status: 'fail', detail: stateIssues.join('; '), fix: 'Run: npx dual-brain repair' });
1035
+ } else if (stateWarns > 0) {
1036
+ results.push({ name: 'State files', status: 'warn', detail: stateIssues.join('; '), fix: 'Run: npx dual-brain repair' });
1037
+ } else {
1038
+ results.push({ name: 'State files', status: 'pass', detail: `${stateHealthy} file(s) healthy` });
1039
+ }
1040
+
1041
+ // 5. Error channel
1042
+ const errorsFile = join(hooksDir, 'errors.jsonl');
1043
+ if (existsSync(errorsFile)) {
1044
+ try {
1045
+ const raw = readFileSync(errorsFile, 'utf8');
1046
+ const lines = raw.split('\n').filter(Boolean);
1047
+ const entries = [];
1048
+ for (const line of lines) {
1049
+ try { entries.push(JSON.parse(line)); } catch {}
1050
+ }
1051
+
1052
+ const cutoff24h = Date.now() - 24 * 60 * 60 * 1000;
1053
+ const recent = entries.filter(e => e.timestamp && Date.parse(e.timestamp) >= cutoff24h);
1054
+
1055
+ if (recent.length === 0) {
1056
+ results.push({ name: 'Error channel', status: 'pass', detail: 'no errors in last 24h' });
1057
+ } else {
1058
+ const last3 = recent.slice(-3);
1059
+ const detail = `${recent.length} error(s) in last 24h`;
1060
+ results.push({ name: 'Error channel', status: 'warn', detail, errors: last3 });
1061
+ }
1062
+ } catch {
1063
+ results.push({ name: 'Error channel', status: 'warn', detail: 'errors.jsonl unreadable' });
1064
+ }
1065
+ } else {
1066
+ results.push({ name: 'Error channel', status: 'pass', detail: 'no errors.jsonl (clean)' });
1067
+ }
1068
+
1069
+ // Output
1070
+ const passCount = results.filter(r => r.status === 'pass').length;
1071
+ const warnCount = results.filter(r => r.status === 'warn').length;
1072
+ const failCount = results.filter(r => r.status === 'fail').length;
1073
+
1074
+ console.log('');
1075
+ console.log(` 🩺 dual-brain v${VERSION} — doctor`);
1076
+ console.log(' ' + '─'.repeat(50));
1077
+
1078
+ for (const r of results) {
1079
+ const icon = r.status === 'pass' ? '✓' : r.status === 'warn' ? '⚠' : '✗';
1080
+ console.log(` ${icon} ${r.name}: ${r.detail}`);
1081
+ if (r.fix) console.log(` → ${r.fix}`);
1082
+ if (r.errors) {
1083
+ for (const e of r.errors) {
1084
+ const ts = e.timestamp ? e.timestamp.slice(11, 19) : '??:??:??';
1085
+ console.log(` ${ts} [${e.hook || '?'}] ${e.error || '?'}`);
1086
+ }
1087
+ }
1088
+ }
1089
+
1090
+ console.log(' ' + '─'.repeat(50));
1091
+ if (failCount > 0) {
1092
+ console.log(` ✗ Needs repair: ${failCount} issue(s) require attention`);
1093
+ } else if (warnCount > 0) {
1094
+ console.log(` ⚠ Warnings: ${warnCount} — system functional but has issues`);
1095
+ } else {
1096
+ console.log(` ✓ Healthy: all ${passCount} checks passed`);
1097
+ }
1098
+ console.log('');
1099
+ }
1100
+
1101
+ // ─── Subcommand: reset ────────────────────────────────────────────────────
1102
+
1103
+ function cmdReset() {
1104
+ const workspace = resolve(process.cwd());
1105
+ const claudeDir = join(workspace, '.claude');
1106
+ const hooksDir = join(claudeDir, 'hooks');
1107
+
1108
+ // Collect state files (NOT config, hooks, or profile)
1109
+ const STATE_FIXED = [
1110
+ join(hooksDir, 'usage.jsonl'),
1111
+ join(hooksDir, 'decision-ledger.jsonl'),
1112
+ join(hooksDir, 'failure-ledger.json'),
1113
+ join(hooksDir, '.burst-state'),
1114
+ join(hooksDir, 'burst-state.json'),
1115
+ join(hooksDir, '.drift-warned'),
1116
+ join(hooksDir, '.budget-alerted'),
1117
+ join(hooksDir, 'errors.jsonl'),
1118
+ join(hooksDir, 'summary-checkpoint.json'),
1119
+ join(claudeDir, '.launched'),
1120
+ ];
1121
+
1122
+ const toDelete = [];
1123
+ for (const f of STATE_FIXED) {
1124
+ if (existsSync(f)) toDelete.push(f);
1125
+ }
1126
+
1127
+ // Scan for date-stamped files and lock files
1128
+ try {
1129
+ for (const f of readdirSync(hooksDir)) {
1130
+ if (f.startsWith('usage-') && f.endsWith('.jsonl')) toDelete.push(join(hooksDir, f));
1131
+ if (f.startsWith('usage-summary-') && f.endsWith('.json')) toDelete.push(join(hooksDir, f));
1132
+ if (f.endsWith('.lock')) toDelete.push(join(hooksDir, f));
1133
+ }
1134
+ } catch {}
1135
+ try {
1136
+ for (const f of readdirSync(claudeDir)) {
1137
+ if (f.endsWith('.lock')) toDelete.push(join(claudeDir, f));
1138
+ }
1139
+ } catch {}
1140
+
1141
+ const unique = [...new Set(toDelete)].filter(f => existsSync(f));
1142
+
1143
+ if (unique.length === 0) {
1144
+ console.log('');
1145
+ console.log(' 🔄 Nothing to reset — no state files found.');
1146
+ console.log('');
1147
+ return;
1148
+ }
1149
+
1150
+ // Require --force or confirmation
1151
+ if (!force) {
1152
+ console.log('');
1153
+ console.log(` 🔄 dual-brain reset — will delete ${unique.length} state file(s):`);
1154
+ console.log('');
1155
+ for (const f of unique) {
1156
+ const rel = f.startsWith(workspace) ? f.slice(workspace.length + 1) : f;
1157
+ console.log(` ${rel}`);
1158
+ }
1159
+ console.log('');
1160
+ console.log(' Preserved: orchestrator.json, settings.json, hooks, profile, CLAUDE.md');
1161
+ console.log('');
1162
+ console.log(` To confirm: ${cmd('npx dual-brain reset --force')}`);
1163
+ console.log('');
1164
+ return;
1165
+ }
1166
+
1167
+ let removed = 0;
1168
+ const errors = [];
1169
+ for (const f of unique) {
1170
+ try {
1171
+ unlinkSync(f);
1172
+ removed++;
1173
+ } catch (err) {
1174
+ errors.push(` ⚠ Could not remove ${f.split('/').pop()}: ${err.message}`);
1175
+ }
1176
+ }
1177
+
1178
+ console.log('');
1179
+ console.log(` 🔄 dual-brain v${VERSION} — reset`);
1180
+ console.log(' ' + '─'.repeat(40));
1181
+ console.log(` ✓ Removed ${removed} state file(s)`);
1182
+ for (const e of errors) console.log(e);
1183
+ console.log('');
1184
+ console.log(' Preserved: orchestrator.json, settings.json, hooks, profile, CLAUDE.md');
1185
+ console.log(' State will rebuild automatically on next session.');
1186
+ console.log('');
1187
+ }
1188
+
1189
+ // ─── Subcommand: repair ───────────────────────────────────────────────────
1190
+
1191
+ function cmdRepair() {
1192
+ const workspace = resolve(process.cwd());
1193
+ const claudeDir = join(workspace, '.claude');
1194
+ const hooksDir = join(claudeDir, 'hooks');
1195
+ const actions = [];
1196
+
1197
+ // 1. Remove stale lock files (older than 30 seconds)
1198
+ let staleLocks = 0;
1199
+ for (const dir of [hooksDir, claudeDir]) {
1200
+ try {
1201
+ for (const f of readdirSync(dir)) {
1202
+ if (!f.endsWith('.lock')) continue;
1203
+ const lockPath = join(dir, f);
1204
+ try {
1205
+ const st = statSync(lockPath);
1206
+ if (Date.now() - st.mtimeMs > 30_000) {
1207
+ unlinkSync(lockPath);
1208
+ staleLocks++;
1209
+ }
1210
+ } catch {}
1211
+ }
1212
+ } catch {}
1213
+ }
1214
+ if (staleLocks > 0) {
1215
+ actions.push(`✓ Removed ${staleLocks} stale lock file(s)`);
1216
+ } else {
1217
+ actions.push('⊘ No stale locks found');
1218
+ }
1219
+
1220
+ // 2. Repair corrupt JSONL files
1221
+ const jsonlFiles = [
1222
+ join(hooksDir, 'decision-ledger.jsonl'),
1223
+ join(hooksDir, 'errors.jsonl'),
1224
+ join(hooksDir, 'usage.jsonl'),
1225
+ ];
1226
+ try {
1227
+ for (const f of readdirSync(hooksDir)) {
1228
+ if (f.startsWith('usage-') && f.endsWith('.jsonl')) {
1229
+ jsonlFiles.push(join(hooksDir, f));
1230
+ }
1231
+ }
1232
+ } catch {}
1233
+
1234
+ let repairedJsonl = 0;
1235
+ for (const filePath of [...new Set(jsonlFiles)]) {
1236
+ if (!existsSync(filePath)) continue;
1237
+ try {
1238
+ const raw = readFileSync(filePath, 'utf8');
1239
+ const lines = raw.split('\n').filter(Boolean);
1240
+ let badCount = 0;
1241
+ const goodLines = [];
1242
+ for (const line of lines) {
1243
+ try {
1244
+ JSON.parse(line);
1245
+ goodLines.push(line);
1246
+ } catch {
1247
+ badCount++;
1248
+ }
1249
+ }
1250
+ if (badCount > 0) {
1251
+ const tmp = filePath + '.tmp.' + process.pid;
1252
+ writeFileSync(tmp, goodLines.length > 0 ? goodLines.join('\n') + '\n' : '');
1253
+ renameSync(tmp, filePath);
1254
+ repairedJsonl++;
1255
+ actions.push(`✓ ${filePath.split('/').pop()}: removed ${badCount} corrupt line(s), kept ${goodLines.length}`);
1256
+ }
1257
+ } catch (err) {
1258
+ actions.push(`⚠ ${filePath.split('/').pop()}: could not repair (${err.message})`);
1259
+ }
1260
+ }
1261
+ if (repairedJsonl === 0) {
1262
+ actions.push('⊘ No corrupt JSONL files found');
1263
+ }
1264
+
1265
+ // 3. Re-validate and fix orchestrator.json formatting
1266
+ const orchPath = join(claudeDir, 'orchestrator.json');
1267
+ if (existsSync(orchPath)) {
1268
+ try {
1269
+ const raw = readFileSync(orchPath, 'utf8');
1270
+ const config = JSON.parse(raw);
1271
+ const pretty = JSON.stringify(config, null, 2) + '\n';
1272
+ if (raw !== pretty) {
1273
+ const tmp = orchPath + '.tmp.' + process.pid;
1274
+ writeFileSync(tmp, pretty);
1275
+ renameSync(tmp, orchPath);
1276
+ actions.push('✓ orchestrator.json: re-formatted');
1277
+ } else {
1278
+ actions.push('⊘ orchestrator.json: already well-formatted');
1279
+ }
1280
+ } catch (err) {
1281
+ actions.push(`✗ orchestrator.json: invalid JSON — ${err.message}`);
1282
+ actions.push(' → Run: npx dual-brain --force (to regenerate from template)');
1283
+ }
1284
+ } else {
1285
+ actions.push('✗ orchestrator.json: not found — run: npx dual-brain');
1286
+ }
1287
+
1288
+ // 4. Re-run hook registration (ensure hooks are in settings.json)
1289
+ if (existsSync(join(claudeDir, 'settings.json')) || existsSync(orchPath)) {
1290
+ try {
1291
+ const settings = generateSettings(workspace);
1292
+ const tmp = join(claudeDir, 'settings.json.tmp.' + process.pid);
1293
+ writeFileSync(tmp, JSON.stringify(settings, null, 2) + '\n');
1294
+ renameSync(tmp, join(claudeDir, 'settings.json'));
1295
+ actions.push('✓ settings.json: hooks re-registered');
1296
+ } catch (err) {
1297
+ actions.push(`⚠ settings.json: could not re-register hooks (${err.message})`);
1298
+ }
1299
+ }
1300
+
1301
+ // Print report
1302
+ console.log('');
1303
+ console.log(` 🔧 dual-brain v${VERSION} — repair`);
1304
+ console.log(' ' + '─'.repeat(40));
1305
+ for (const a of actions) {
1306
+ console.log(` ${a}`);
1307
+ }
1308
+ console.log('');
1309
+ }
1310
+
701
1311
  // ─── Uninstall ─────────────────────────────────────────────────────────────
702
1312
 
703
1313
  function cmdUninstall() {
@@ -817,9 +1427,140 @@ function cmdUninstall() {
817
1427
  console.log('');
818
1428
  }
819
1429
 
1430
+ // ─── Hook Delegation ───────────────────────────────────────────────────────
1431
+
1432
+ const HOOK_COMMANDS = {
1433
+ review: 'dual-brain-review.mjs',
1434
+ think: 'dual-brain-think.mjs',
1435
+ health: 'health-check.mjs',
1436
+ report: 'session-report.mjs',
1437
+ gate: 'quality-gate.mjs',
1438
+ vibe: 'vibe-router.mjs',
1439
+ plan: 'plan-generator.mjs',
1440
+ cost: 'cost-report.mjs',
1441
+ dispatch: 'gpt-work-dispatcher.mjs',
1442
+ memory: 'vibe-memory.mjs',
1443
+ test: 'test-orchestrator.mjs',
1444
+ ledger: 'decision-ledger.mjs',
1445
+ chains: 'agent-chains.mjs',
1446
+ };
1447
+
1448
+ function resolveHookScript(hookFile) {
1449
+ const workspace = resolve(process.cwd());
1450
+ const installed = join(workspace, '.claude', 'hooks', hookFile);
1451
+ const bundled = join(__dirname, 'hooks', hookFile);
1452
+ return existsSync(installed) ? installed : existsSync(bundled) ? bundled : null;
1453
+ }
1454
+
1455
+ function delegateToHook(hookFile) {
1456
+ const script = resolveHookScript(hookFile);
1457
+
1458
+ if (!script) {
1459
+ console.error(` Hook not found: ${hookFile}`);
1460
+ console.error(` Run: ${cmd('npx dual-brain')} to install hooks first.`);
1461
+ process.exit(1);
1462
+ }
1463
+
1464
+ // Pass through all args after the subcommand
1465
+ const extraArgs = process.argv.slice(3);
1466
+ const { status } = spawnSync(process.execPath, [script, ...extraArgs], {
1467
+ stdio: 'inherit',
1468
+ cwd: resolve(process.cwd()),
1469
+ });
1470
+ process.exit(status || 0);
1471
+ }
1472
+
1473
+ function delegateToHookWithArgs(hookFile, args) {
1474
+ const script = resolveHookScript(hookFile);
1475
+
1476
+ if (!script) {
1477
+ console.error(` Hook not found: ${hookFile}`);
1478
+ console.error(` Run: ${cmd('npx dual-brain')} to install hooks first.`);
1479
+ process.exit(1);
1480
+ }
1481
+
1482
+ const { status } = spawnSync(process.execPath, [script, ...args], {
1483
+ stdio: 'inherit',
1484
+ cwd: resolve(process.cwd()),
1485
+ });
1486
+ process.exit(status || 0);
1487
+ }
1488
+
820
1489
  // ─── Main ───────────────────────────────────────────────────────────────────
821
1490
 
822
- function main() {
1491
+ // ─── Replit-Tools Check ────────────────────────────────────────────────────
1492
+
1493
+ async function checkReplitTools() {
1494
+ // Only run this check when actually on Replit
1495
+ if (!IS_REPLIT) return;
1496
+
1497
+ // Check if replit-tools is installed
1498
+ const hasReplitTools = existsSync(resolve(process.cwd(), '.replit-tools'));
1499
+ if (hasReplitTools) return;
1500
+
1501
+ const yesFlag = flag('--yes') || flag('-y');
1502
+
1503
+ console.log('');
1504
+ console.log(' ━━━ replit-tools Required ━━━');
1505
+ console.log('');
1506
+ console.log(' dual-brain is built on data-tools/replit-tools by Steve Moraco.');
1507
+ console.log(' replit-tools provides persistent auth, session management, and');
1508
+ console.log(' container survival that dual-brain depends on.');
1509
+ console.log('');
1510
+
1511
+ let install = yesFlag;
1512
+
1513
+ if (!install) {
1514
+ process.stdout.write(' Install now? [Y/n] ');
1515
+
1516
+ if (process.stdin.isTTY) {
1517
+ install = await new Promise((resolve) => {
1518
+ process.stdin.setEncoding('utf8');
1519
+ process.stdin.once('data', (chunk) => {
1520
+ const answer = chunk.trim().toLowerCase();
1521
+ // Default yes: empty input (just Enter) or explicit y/yes
1522
+ resolve(answer === '' || answer === 'y' || answer === 'yes');
1523
+ });
1524
+ process.stdin.resume();
1525
+ });
1526
+ process.stdin.pause();
1527
+ } else {
1528
+ // Non-TTY: default to yes
1529
+ console.log('');
1530
+ install = true;
1531
+ }
1532
+ }
1533
+
1534
+ console.log('');
1535
+
1536
+ if (install) {
1537
+ console.log(' Installing replit-tools...');
1538
+ console.log('');
1539
+ const result = spawnSync('npx', ['-y', 'replit-tools'], { stdio: 'inherit', shell: true });
1540
+ console.log('');
1541
+ if (result.status !== 0) {
1542
+ console.log(' ⚠ replit-tools install exited with a non-zero status.');
1543
+ console.log(' Continuing with dual-brain install anyway.');
1544
+ console.log('');
1545
+ } else {
1546
+ // Re-check that replit-tools is now present
1547
+ if (existsSync(resolve(process.cwd(), '.replit-tools'))) {
1548
+ console.log(' ✓ replit-tools installed successfully.');
1549
+ } else {
1550
+ console.log(' replit-tools may need a shell reload. Continuing with dual-brain install.');
1551
+ }
1552
+ console.log('');
1553
+ }
1554
+ } else {
1555
+ console.log(' dual-brain will work with reduced functionality. Some features');
1556
+ console.log(' (persistent state, session resume, auth survival) require replit-tools.');
1557
+ console.log('');
1558
+ }
1559
+ }
1560
+
1561
+ // ─── Main ───────────────────────────────────────────────────────────────────
1562
+
1563
+ async function main() {
823
1564
  if (flag('--uninstall')) { cmdUninstall(); return; }
824
1565
 
825
1566
  if (subcommand === 'status') {
@@ -829,6 +1570,287 @@ function main() {
829
1570
  if (subcommand === 'mode') { cmdMode(); return; }
830
1571
  if (subcommand === 'budget') { cmdBudget(); return; }
831
1572
  if (subcommand === 'explain') { cmdExplain(); return; }
1573
+ if (subcommand === 'doctor') { cmdDoctor(); return; }
1574
+ if (subcommand === 'reset') { cmdReset(); return; }
1575
+ if (subcommand === 'repair') { cmdRepair(); return; }
1576
+
1577
+ // agent <template> [flags] → agent-templates.mjs --run <template> [flags]
1578
+ if (subcommand === 'agent') {
1579
+ const templateName = positional[1];
1580
+ if (!templateName) {
1581
+ // No template name — fall through to list
1582
+ delegateToHookWithArgs('agent-templates.mjs', ['--list']);
1583
+ return;
1584
+ }
1585
+ const extraFlags = process.argv.slice(4); // skip node, install.mjs, 'agent', <template>
1586
+ delegateToHookWithArgs('agent-templates.mjs', ['--run', templateName, ...extraFlags]);
1587
+ return;
1588
+ }
1589
+
1590
+ // agents → agent-templates.mjs --list
1591
+ if (subcommand === 'agents') {
1592
+ delegateToHookWithArgs('agent-templates.mjs', ['--list']);
1593
+ return;
1594
+ }
1595
+
1596
+ // chain <name> [flags] → agent-chains.mjs --run <name> [flags]
1597
+ if (subcommand === 'chain') {
1598
+ const chainName = positional[1];
1599
+ if (!chainName) {
1600
+ // No chain name given — fall through to list
1601
+ delegateToHookWithArgs('agent-chains.mjs', ['--list']);
1602
+ return;
1603
+ }
1604
+ const extraFlags = process.argv.slice(4); // skip node, install.mjs, 'chain', <name>
1605
+ delegateToHookWithArgs('agent-chains.mjs', ['--run', chainName, ...extraFlags]);
1606
+ return;
1607
+ }
1608
+
1609
+ // chains → agent-chains.mjs --list
1610
+ if (subcommand === 'chains') {
1611
+ delegateToHookWithArgs('agent-chains.mjs', ['--list']);
1612
+ return;
1613
+ }
1614
+
1615
+ // ─── Ship Captain commands ─────────────────────────────────────────────────
1616
+
1617
+ // do "<goal>" → ship-captain.mjs --goal "<goal>" [remaining flags]
1618
+ if (subcommand === 'do') {
1619
+ const goal = positional[1] || '';
1620
+ if (!goal) {
1621
+ console.error(' Usage: npx dual-brain do "<goal>"');
1622
+ process.exit(1);
1623
+ }
1624
+ // Collect remaining flags (everything after the goal string)
1625
+ const extraFlags = process.argv.slice(4).filter(a => a.startsWith('-'));
1626
+ delegateToHookWithArgs('ship-captain.mjs', ['--goal', goal, ...extraFlags]);
1627
+ return;
1628
+ }
1629
+
1630
+ // ship → ship-gate.mjs --ship [remaining flags]
1631
+ if (subcommand === 'ship') {
1632
+ const extraFlags = process.argv.slice(3).filter(a => a.startsWith('-'));
1633
+ delegateToHookWithArgs('ship-gate.mjs', ['--ship', ...extraFlags]);
1634
+ return;
1635
+ }
1636
+
1637
+ // test-run → ship-gate.mjs --test-only
1638
+ if (subcommand === 'test-run') {
1639
+ const extraFlags = process.argv.slice(3).filter(a => a.startsWith('-'));
1640
+ delegateToHookWithArgs('ship-gate.mjs', ['--test-only', ...extraFlags]);
1641
+ return;
1642
+ }
1643
+
1644
+ // diff → ship-gate.mjs --diff-only
1645
+ if (subcommand === 'diff') {
1646
+ delegateToHookWithArgs('ship-gate.mjs', ['--diff-only']);
1647
+ return;
1648
+ }
1649
+
1650
+ // runs → list recent Ship Captain runs from .claude/runs/
1651
+ if (subcommand === 'runs') {
1652
+ const workspace = resolve(process.cwd());
1653
+ const runsDir = join(workspace, '.claude', 'runs');
1654
+ if (!existsSync(runsDir)) {
1655
+ console.log('');
1656
+ console.log(' No Ship Captain runs found.');
1657
+ console.log(` Run: ${cmd('npx dual-brain do "<goal>"')} to start your first run.`);
1658
+ console.log('');
1659
+ return;
1660
+ }
1661
+ let files;
1662
+ try {
1663
+ files = readdirSync(runsDir).filter(f => f.endsWith('.json')).sort().reverse();
1664
+ } catch {
1665
+ files = [];
1666
+ }
1667
+ if (files.length === 0) {
1668
+ console.log('');
1669
+ console.log(' No run records found in .claude/runs/');
1670
+ console.log('');
1671
+ return;
1672
+ }
1673
+ const rows = [];
1674
+ for (const f of files) {
1675
+ try {
1676
+ const rec = JSON.parse(readFileSync(join(runsDir, f), 'utf8'));
1677
+ const id = rec.id || f.replace('.json', '');
1678
+ const goal = (rec.goal || '').slice(0, 35);
1679
+ const status = rec.status || '?';
1680
+ const steps = Array.isArray(rec.steps) ? rec.steps.length : (rec.step_count || '?');
1681
+ const dur = rec.duration_ms != null ? `${(rec.duration_ms / 1000).toFixed(1)}s` : rec.duration || '?';
1682
+ const date = rec.started_at ? rec.started_at.slice(0, 16).replace('T', ' ') : (rec.date || '?');
1683
+ rows.push({ id, goal, status, steps, dur, date });
1684
+ } catch {}
1685
+ }
1686
+ if (rows.length === 0) {
1687
+ console.log(' No readable run records.');
1688
+ return;
1689
+ }
1690
+ const colW = { id: 14, goal: 37, status: 10, steps: 6, dur: 8, date: 16 };
1691
+ const header = [
1692
+ 'ID'.padEnd(colW.id), 'GOAL'.padEnd(colW.goal), 'STATUS'.padEnd(colW.status),
1693
+ 'STEPS'.padStart(colW.steps), 'DUR'.padStart(colW.dur), 'DATE'.padEnd(colW.date),
1694
+ ].join(' ');
1695
+ console.log('');
1696
+ console.log(` Ship Captain Runs (${rows.length})`);
1697
+ console.log(' ' + '─'.repeat(header.length));
1698
+ console.log(' ' + header);
1699
+ console.log(' ' + '─'.repeat(header.length));
1700
+ for (const r of rows) {
1701
+ const line = [
1702
+ String(r.id).padEnd(colW.id), String(r.goal).padEnd(colW.goal),
1703
+ String(r.status).padEnd(colW.status), String(r.steps).padStart(colW.steps),
1704
+ String(r.dur).padStart(colW.dur), String(r.date).padEnd(colW.date),
1705
+ ].join(' ');
1706
+ console.log(' ' + line);
1707
+ }
1708
+ console.log('');
1709
+ return;
1710
+ }
1711
+
1712
+ // resume → find most recent incomplete/failed run, offer to re-execute from the failed step
1713
+ if (subcommand === 'resume') {
1714
+ const workspace = resolve(process.cwd());
1715
+ const runsDir = join(workspace, '.claude', 'runs');
1716
+ if (!existsSync(runsDir)) {
1717
+ console.log('');
1718
+ console.log(" No runs found. Start with: npx dual-brain do '<goal>'");
1719
+ console.log('');
1720
+ return;
1721
+ }
1722
+ let files;
1723
+ try {
1724
+ files = readdirSync(runsDir).filter(f => f.endsWith('.json')).sort().reverse();
1725
+ } catch {
1726
+ files = [];
1727
+ }
1728
+ if (files.length === 0) {
1729
+ console.log('');
1730
+ console.log(" No runs found. Start with: npx dual-brain do '<goal>'");
1731
+ console.log('');
1732
+ return;
1733
+ }
1734
+
1735
+ // Load the most recent run record
1736
+ let rec = null;
1737
+ let recFile = null;
1738
+ try {
1739
+ recFile = files[0];
1740
+ rec = JSON.parse(readFileSync(join(runsDir, recFile), 'utf8'));
1741
+ } catch {}
1742
+
1743
+ if (!rec) {
1744
+ console.log('');
1745
+ console.log(" No runs found. Start with: npx dual-brain do '<goal>'");
1746
+ console.log('');
1747
+ return;
1748
+ }
1749
+
1750
+ const INCOMPLETE_STATUSES = ['failed', 'error', 'partial', 'running', 'pending', 'incomplete', 'aborted'];
1751
+ const steps = Array.isArray(rec.steps) ? rec.steps : [];
1752
+ const isCompleted = (rec.status || '').toLowerCase() === 'completed';
1753
+ const allDone = steps.length > 0 && steps.every(s =>
1754
+ s.status === 'done' || s.status === 'complete' || s.status === 'skipped'
1755
+ );
1756
+
1757
+ if (isCompleted && (allDone || steps.length === 0)) {
1758
+ const goal = rec.goal || '(unknown)';
1759
+ console.log('');
1760
+ console.log(' Last run completed successfully.');
1761
+ console.log(` Start a new one with: npx dual-brain do '${goal}'`);
1762
+ console.log('');
1763
+ return;
1764
+ }
1765
+
1766
+ // Run has failed/aborted/incomplete — print summary
1767
+ const done = steps.filter(s => s.status === 'done' || s.status === 'complete').length;
1768
+ const failedSteps = steps.filter(s => s.status === 'failed' || s.status === 'error');
1769
+ const incompleteIdx = steps.findIndex(s =>
1770
+ s.status !== 'done' && s.status !== 'complete' && s.status !== 'skipped'
1771
+ );
1772
+ const resumeFromIdx = incompleteIdx >= 0 ? incompleteIdx : steps.length;
1773
+
1774
+ console.log('');
1775
+ console.log(' Last Run State');
1776
+ console.log(' ' + '─'.repeat(50));
1777
+ console.log(` ID: ${rec.id || recFile.replace('.json', '')}`);
1778
+ console.log(` Goal: ${rec.goal || '(unknown)'}`);
1779
+ console.log(` Status: ${rec.status || '(unknown)'}`);
1780
+ if (steps.length > 0) {
1781
+ console.log(` Steps: ${done}/${steps.length} done, ${failedSteps.length} failed`);
1782
+ const failedStep = failedSteps[0];
1783
+ if (failedStep) {
1784
+ const fi = steps.indexOf(failedStep);
1785
+ const fname = failedStep.task || failedStep.name || failedStep.label || `step ${fi}`;
1786
+ console.log(` Failed: ${fname}`);
1787
+ if (failedStep.error) console.log(` Error: ${failedStep.error}`);
1788
+ }
1789
+ }
1790
+ if (rec.started_at) console.log(` Started: ${rec.started_at.slice(0, 16).replace('T', ' ')}`);
1791
+ if (rec.error) console.log(` Error: ${rec.error}`);
1792
+ console.log('');
1793
+
1794
+ const isResumable = INCOMPLETE_STATUSES.includes((rec.status || '').toLowerCase()) && rec.goal;
1795
+
1796
+ if (!isResumable) {
1797
+ console.log(" Use npx dual-brain do '<goal>' to start fresh");
1798
+ console.log('');
1799
+ return;
1800
+ }
1801
+
1802
+ const stepNum = resumeFromIdx + 1;
1803
+ const totalSteps = steps.length || '?';
1804
+ console.log(` Resume from step ${stepNum}/${totalSteps}? [Y/n]`);
1805
+
1806
+ const yesFlag = flag('--yes') || flag('-y');
1807
+
1808
+ if (!yesFlag && process.stdin.isTTY) {
1809
+ const { createInterface } = await import('readline');
1810
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
1811
+ const answer = await new Promise((res) => {
1812
+ rl.question(' > ', (a) => { rl.close(); res(a.trim()); });
1813
+ });
1814
+ if (answer !== '' && !/^y(es)?$/i.test(answer)) {
1815
+ console.log('');
1816
+ console.log(` Use npx dual-brain do '${rec.goal}' to start fresh`);
1817
+ console.log('');
1818
+ return;
1819
+ }
1820
+ }
1821
+
1822
+ // Reconstruct flags from stored options in the run record
1823
+ const storedOpts = rec.options || {};
1824
+
1825
+ // Dynamically import and invoke ship-captain's executeShipCaptain
1826
+ const captainPath = resolveHookScript('ship-captain.mjs');
1827
+ if (!captainPath) {
1828
+ console.error(' ship-captain.mjs not found. Run: npx dual-brain to reinstall hooks.');
1829
+ process.exit(1);
1830
+ }
1831
+ const { executeShipCaptain } = await import(captainPath);
1832
+ await executeShipCaptain(rec.goal, {
1833
+ yes: yesFlag || storedOpts.yes || false,
1834
+ yolo: storedOpts.yolo || false,
1835
+ careful: storedOpts.careful || false,
1836
+ noPr: storedOpts.noPr || false,
1837
+ mode: storedOpts.mode || null,
1838
+ resumeFrom: resumeFromIdx,
1839
+ resumedFromId: rec.id,
1840
+ });
1841
+ return;
1842
+ }
1843
+
1844
+ // Delegate hook-backed commands
1845
+ if (subcommand && HOOK_COMMANDS[subcommand]) {
1846
+ delegateToHook(HOOK_COMMANDS[subcommand]);
1847
+ return;
1848
+ }
1849
+
1850
+ // ── replit-tools check — first thing before auth detection or install ──
1851
+ if (!dryRun && !jsonOut) {
1852
+ await checkReplitTools();
1853
+ }
832
1854
 
833
1855
  const env = detectEnvironment();
834
1856
  const mode = resolveMode(env);
@@ -842,23 +1864,34 @@ function main() {
842
1864
  process.exit(0);
843
1865
  }
844
1866
 
845
- // Check for replit-tools on Replit
846
- if (env.isReplit && !env.hasReplitTools) {
847
- console.log('');
848
- console.log(' ⚠️ replit-tools not found recommended for Replit environments.');
849
- console.log(' Dual-brain works best alongside replit-tools for persistent auth,');
850
- console.log(' session management, and shell integration.');
851
- console.log('');
852
- console.log(` Install: ${cmd('npx -y data-tools')}`);
853
- console.log('');
1867
+ // Guided auth: print instructions if a provider is missing/not authed
1868
+ // and offer to wait (interactive only)
1869
+ const needsGuidance = printGuidedAuth(env);
1870
+ if (needsGuidance && process.stdin.isTTY && process.stdout.isTTY && !process.env.CI) {
1871
+ await waitForAuth(env);
1872
+ // Re-resolve mode after potential auth
1873
+ Object.assign(mode, resolveMode(env));
854
1874
  }
855
1875
 
856
1876
  const actions = install(env.workspace, env, mode);
857
1877
  printReport(env, mode, actions);
858
1878
 
859
- // After install, launch the session manager (interactive TTY only)
1879
+ // Always print quick-start block after successful install
1880
+ printQuickStart();
1881
+
1882
+ // Offer to launch control panel (opt-in, interactive TTY only)
860
1883
  if (process.stdin.isTTY && process.stdout.isTTY && !process.env.CI) {
861
- launchPanel();
1884
+ process.stdout.write(' Launch control panel? [y/N] ');
1885
+ const answer = await new Promise((resolve) => {
1886
+ process.stdin.setEncoding('utf8');
1887
+ process.stdin.once('data', (chunk) => resolve(chunk.trim().toLowerCase()));
1888
+ process.stdin.resume();
1889
+ });
1890
+ process.stdin.pause();
1891
+ console.log('');
1892
+ if (answer === 'y' || answer === 'yes') {
1893
+ launchPanel();
1894
+ }
862
1895
  }
863
1896
  }
864
1897