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/CLAUDE.md +130 -35
- package/README.md +171 -44
- package/hooks/agent-chains.mjs +369 -0
- package/hooks/agent-templates.mjs +441 -0
- package/hooks/atomic-write.mjs +5 -3
- package/hooks/config-validator.mjs +156 -0
- package/hooks/confirmation-policy.mjs +167 -0
- package/hooks/cost-logger.mjs +32 -12
- package/hooks/cost-report.mjs +60 -114
- package/hooks/decision-ledger.mjs +3 -2
- package/hooks/dual-brain-review.mjs +249 -2
- package/hooks/dual-brain-think.mjs +294 -25
- package/hooks/enforce-tier.mjs +246 -87
- package/hooks/error-channel.mjs +68 -0
- package/hooks/failure-detector.mjs +2 -1
- package/hooks/health-check.mjs +16 -17
- package/hooks/risk-classifier.mjs +135 -2
- package/hooks/session-report.mjs +41 -71
- package/hooks/ship-captain.mjs +1176 -0
- package/hooks/ship-gate.mjs +971 -0
- package/hooks/summary-checkpoint.mjs +31 -4
- package/hooks/test-orchestrator.mjs +1975 -11
- package/install.mjs +1064 -31
- package/orchestrator.json +73 -96
- package/package.json +7 -2
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
|
-
|
|
47
|
-
(none)
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
|
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 = [
|
|
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
|
-
|
|
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
|
-
//
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|