dual-brain 3.1.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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, writeFileSync } from 'fs';
11
+ import { cpSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'fs';
12
12
  import { dirname, join, resolve } from 'path';
13
13
  import { fileURLToPath } from 'url';
14
14
  import { spawnSync } from 'child_process';
@@ -16,6 +16,12 @@ import { spawnSync } from 'child_process';
16
16
  const __dirname = dirname(fileURLToPath(import.meta.url));
17
17
  const VERSION = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf8')).version;
18
18
 
19
+ // ─── Replit Detection ──────────────────────────────────────────────────────
20
+
21
+ const IS_REPLIT = !!(process.env.REPL_ID || process.env.REPL_SLUG);
22
+
23
+ function cmd(s) { return IS_REPLIT ? `! ${s}` : s; }
24
+
19
25
  // ─── CLI ────────────────────────────────────────────────────────────────────
20
26
 
21
27
  const argv = process.argv.slice(2);
@@ -23,6 +29,8 @@ const flag = (f) => argv.includes(f);
23
29
  const force = flag('--force');
24
30
  const dryRun = flag('--dry-run');
25
31
  const jsonOut = flag('--json');
32
+ const positional = argv.filter(a => !a.startsWith('-'));
33
+ const subcommand = positional[0] || null;
26
34
 
27
35
  if (flag('--version') || flag('-v')) {
28
36
  console.log(`dual-brain v${VERSION}`);
@@ -31,24 +39,43 @@ if (flag('--version') || flag('-v')) {
31
39
 
32
40
  if (flag('--help') || flag('-h')) {
33
41
  console.log(`
34
- dual-brain v${VERSION} — Dual-provider orchestrator for Claude Code
42
+ 🧠 dual-brain v${VERSION} — Dual-provider orchestrator for Claude Code
43
+
44
+ Usage: npx -y dual-brain [command] [options]
35
45
 
36
- Usage: npx -y dual-brain [options]
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
52
+ init Alias for default install
37
53
 
38
54
  Options:
39
- --force Overwrite all existing config (keeps review-rules.md)
40
- --dry-run Detect environment only, don't install
41
- --json Output detection as JSON (implies --dry-run)
55
+ --force Overwrite all existing config
56
+ --dry-run Detect environment only
57
+ --json Output detection as JSON
42
58
  --help Show this help
59
+
60
+ 🎛️ Profiles:
61
+ ⚖️ balanced Standard routing — best model per tier
62
+ 💸 cost-saver Minimize spend — prefer cheaper models
63
+ 💎 quality-first Maximum quality — dual-brain for medium+
64
+
65
+ 🚀 Examples:
66
+ ${cmd('npx dual-brain')} # install or update
67
+ ${cmd('npx dual-brain status')} # open control panel
68
+ ${cmd('npx dual-brain mode cost-saver')} # switch profile
69
+ ${cmd('npx dual-brain budget 8 25')} # \$8 session / \$25 daily
70
+ ${cmd('npx dual-brain explain')} # last routing decision
43
71
  `);
44
72
  process.exit(0);
45
73
  }
46
74
 
47
- // Silently accept 'init' for backward compat
48
- const positional = argv.filter(a => !a.startsWith('-'));
49
- if (positional.length > 0 && positional[0] !== 'init') {
50
- console.error(` Unknown command: ${positional[0]}`);
51
- console.error(' Run: npx dual-brain --help');
75
+ const SUBCOMMANDS = ['init', 'status', 'mode', 'budget', 'explain'];
76
+ if (subcommand && !SUBCOMMANDS.includes(subcommand)) {
77
+ console.error(` Unknown command: ${subcommand}`);
78
+ console.error(` Run: ${cmd('npx dual-brain --help')}`);
52
79
  process.exit(1);
53
80
  }
54
81
 
@@ -283,6 +310,9 @@ function generateGitignoreEntries(workspace) {
283
310
  '.claude/reviews/',
284
311
  '.claude/hooks/.drift-warned',
285
312
  '.claude/hooks/.budget-alerted',
313
+ '.claude/dual-brain.profile.json',
314
+ '.claude/hooks/usage-summary-*.json',
315
+ '.claude/hooks/decision-ledger.jsonl',
286
316
  ];
287
317
  let existing = '';
288
318
  try { existing = readFileSync(join(workspace, '.gitignore'), 'utf8'); } catch {}
@@ -303,7 +333,8 @@ function install(workspace, env, mode) {
303
333
  'dual-brain-review.mjs', 'dual-brain-think.mjs', 'quality-gate.mjs',
304
334
  'test-orchestrator.mjs', 'setup-wizard.mjs', 'health-check.mjs',
305
335
  'install-git-hooks.mjs', 'session-report.mjs', 'budget-balancer.mjs',
306
- 'gpt-work-dispatcher.mjs',
336
+ 'gpt-work-dispatcher.mjs', 'profiles.mjs',
337
+ 'summary-checkpoint.mjs', 'decision-ledger.mjs',
307
338
  ];
308
339
  for (const h of HOOKS) cpSync(join(__dirname, 'hooks', h), join(target, 'hooks', h));
309
340
  actions.push(`✓ ${HOOKS.length} hook scripts`);
@@ -350,43 +381,50 @@ function install(workspace, env, mode) {
350
381
 
351
382
  // ─── Status Report ──────────────────────────────────────────────────────────
352
383
 
353
- function statusIcon(val) { return val ? '' : ''; }
384
+ function statusIcon(val) { return val ? '' : ''; }
385
+
386
+ const MODE_EMOJIS = {
387
+ 'dual': '🧠',
388
+ 'claude-only': '🟠',
389
+ 'openai-only': '🟢',
390
+ 'detect-only': '🔎',
391
+ };
354
392
 
355
393
  function printReport(env, mode, actions) {
356
394
  const lines = [];
357
395
 
358
396
  lines.push(br('╔', '╗'));
359
- lines.push(ln(`Dual-Brain Orchestrator v${VERSION}`));
397
+ lines.push(ln(`🧠 Dual-Brain Orchestrator v${VERSION}`));
360
398
  lines.push(sep());
361
399
 
362
- lines.push(ln('Environment'));
400
+ lines.push(ln('🌎 Environment'));
363
401
  if (env.isReplit) {
364
- lines.push(ln(` Platform: Replit${env.hasReplitTools ? ' (replit-tools detected)' : ''}`));
402
+ lines.push(ln(` 🌀 Platform: Replit${env.hasReplitTools ? ' + replit-tools' : ''}`));
365
403
  } else {
366
404
  lines.push(ln(' Platform: standalone'));
367
405
  }
368
406
 
369
407
  const cVer = env.claude.version ? ` ${env.claude.version}` : '';
370
- const cAuth = env.claude.authed ? 'authenticated' : env.claude.installed ? 'not authenticated' : 'not found';
371
- lines.push(ln(` Claude CLI: ${statusIcon(env.claude.authed)} ${cAuth}${cVer}`));
408
+ const cAuth = env.claude.authed ? 'authenticated' : env.claude.installed ? '⚠️ login needed' : 'not found';
409
+ lines.push(ln(` 🟠 Claude: ${cAuth}${cVer}`));
372
410
 
373
411
  const xVer = env.codex.version ? ` ${env.codex.version}` : '';
374
- const xAuth = env.codex.authed ? 'authenticated' : env.codex.installed ? 'not authenticated' : 'not found';
375
- lines.push(ln(` Codex CLI: ${statusIcon(env.codex.authed)} ${xAuth}${xVer}`));
412
+ const xAuth = env.codex.authed ? 'authenticated' : env.codex.installed ? '⚠️ login needed' : 'not found';
413
+ lines.push(ln(` 🟢 Codex: ${xAuth}${xVer}`));
376
414
 
377
415
  lines.push(sep());
378
- lines.push(ln(`Mode: ${MODE_LABELS[mode.mode]}`));
416
+ lines.push(ln(`${MODE_EMOJIS[mode.mode] || '🧠'} Mode: ${MODE_LABELS[mode.mode]}`));
379
417
 
380
418
  if (actions) {
381
419
  lines.push(sep());
382
- lines.push(ln('Installed'));
420
+ lines.push(ln('📝 Installed'));
383
421
  for (const a of actions) lines.push(ln(` ${a}`));
384
422
  }
385
423
 
386
424
  const needsAction = !env.claude.authed || !env.codex.authed;
387
425
  if (needsAction && mode.mode !== 'dual') {
388
426
  lines.push(sep());
389
- lines.push(ln('To unlock full features:'));
427
+ lines.push(ln('🔓 Unlock full power:'));
390
428
  if (!env.claude.installed) {
391
429
  lines.push(ln(' curl -fsSL https://claude.ai/install.sh | sh'));
392
430
  }
@@ -405,8 +443,8 @@ function printReport(env, mode, actions) {
405
443
  lines.push(sep());
406
444
  if (actions) {
407
445
  lines.push(ln(mode.mode === 'dual'
408
- ? 'Ready both providers active, no restart needed'
409
- : 'Ready hooks active, run commands above for full power'));
446
+ ? 'Ready: both providers active, no restart needed'
447
+ : 'Ready: hooks active, run commands above for full power'));
410
448
  } else {
411
449
  lines.push(ln('Dry run — no files written'));
412
450
  }
@@ -417,32 +455,363 @@ function printReport(env, mode, actions) {
417
455
  console.log('');
418
456
 
419
457
  if (actions) {
420
- console.log(' What just happened:');
421
- console.log(' Every Claude Code session in this project now auto-routes');
422
- console.log(' agent work by complexity — cheap models for search, mid-tier');
423
- console.log(' for execution, best models for thinking. Cost is tracked.');
458
+ console.log(' 🧭 What changed:');
459
+ console.log(' Every Claude Code session now auto-routes agent work by');
460
+ console.log(' complexity — cheap models for search, mid-tier for execution,');
461
+ console.log(' best models for thinking. Cost is tracked automatically.');
424
462
  if (mode.mode === 'dual') {
425
- console.log(' Both Claude and GPT are available as work providers.');
463
+ console.log(' 🧠 Both Claude and GPT are available as work providers.');
426
464
  }
427
465
  console.log('');
428
- console.log(' Try these in your next Claude Code session:');
466
+ console.log(' ⌨️ Open the control panel:');
467
+ console.log(` ${cmd('npx dual-brain status')}`);
468
+ console.log('');
469
+ console.log(' 🩺 In-session tools (ask Claude to run):');
429
470
  console.log(' node .claude/hooks/health-check.mjs # verify setup');
430
471
  console.log(' node .claude/hooks/cost-report.mjs # see activity');
431
- console.log(' node .claude/hooks/budget-balancer.mjs # provider balance');
472
+ console.log(' node .claude/hooks/decision-ledger.mjs # routing insights');
432
473
  if (mode.openaiEnabled) {
433
474
  console.log(' node .claude/hooks/dual-brain-review.mjs # GPT code review');
434
475
  }
435
476
  console.log('');
436
- console.log(' Customize:');
477
+ console.log(' ⚙️ Customize:');
437
478
  console.log(' .claude/review-rules.md # your project\'s review rules');
438
479
  console.log(' .claude/orchestrator.json # routing, budgets, tiers');
439
480
  console.log('');
440
481
  }
441
482
  }
442
483
 
484
+ // ─── Profile System ────────────────────────────────────────────────────────
485
+
486
+ const PROFILE_FILE_REL = '.claude/dual-brain.profile.json';
487
+
488
+ function profilePath(workspace) {
489
+ return join(workspace || process.cwd(), PROFILE_FILE_REL);
490
+ }
491
+
492
+ const PROFILES = {
493
+ balanced: {
494
+ description: 'Standard routing — best model for each tier, normal budgets',
495
+ routing: { prefer_provider: 'auto', think_threshold: 'normal', gpt_dispatch_bias: 0 },
496
+ budgets: { session_warn_usd: 5, session_limit_usd: 10, daily_warn_usd: 20, daily_limit_usd: 50 },
497
+ quality_gate: { sensitivity_floor: 'medium', dual_brain_minimum: 'high' },
498
+ },
499
+ 'cost-saver': {
500
+ description: 'Minimize spend — prefer cheaper models, skip GPT for low risk',
501
+ routing: { prefer_provider: 'cheapest', think_threshold: 'strict', gpt_dispatch_bias: -20 },
502
+ budgets: { session_warn_usd: 2, session_limit_usd: 5, daily_warn_usd: 8, daily_limit_usd: 20 },
503
+ quality_gate: { sensitivity_floor: 'high', dual_brain_minimum: 'critical' },
504
+ },
505
+ 'quality-first': {
506
+ description: 'Maximum quality — dual-brain for medium+, stricter reviews',
507
+ routing: { prefer_provider: 'most-capable', think_threshold: 'relaxed', gpt_dispatch_bias: 10 },
508
+ budgets: { session_warn_usd: 15, session_limit_usd: 30, daily_warn_usd: 50, daily_limit_usd: 100 },
509
+ quality_gate: { sensitivity_floor: 'low', dual_brain_minimum: 'medium' },
510
+ },
511
+ };
512
+
513
+ function loadProfile(workspace) {
514
+ try {
515
+ const data = JSON.parse(readFileSync(profilePath(workspace), 'utf8'));
516
+ const name = data.active && PROFILES[data.active] ? data.active : 'balanced';
517
+ const profile = PROFILES[name];
518
+ const custom = data.custom_overrides || {};
519
+ return {
520
+ name,
521
+ ...profile,
522
+ budgets: { ...profile.budgets, ...custom.budgets },
523
+ routing: { ...profile.routing, ...custom.routing },
524
+ switched_at: data.switched_at || null,
525
+ };
526
+ } catch {
527
+ return { name: 'balanced', ...PROFILES.balanced, switched_at: null };
528
+ }
529
+ }
530
+
531
+ function saveProfile(workspace, name, customOverrides) {
532
+ const data = { active: name, switched_at: new Date().toISOString() };
533
+ if (customOverrides) data.custom_overrides = customOverrides;
534
+ const target = profilePath(workspace);
535
+ const tmp = target + '.tmp.' + process.pid;
536
+ writeFileSync(tmp, JSON.stringify(data, null, 2) + '\n');
537
+ renameSync(tmp, target);
538
+ }
539
+
540
+ // ─── Subcommand: status ────────────────────────────────────────────────────
541
+
542
+ function cmdStatus() {
543
+ const workspace = resolve(process.cwd());
544
+ const env = detectEnvironment();
545
+ const mode = resolveMode(env);
546
+ const profile = loadProfile(workspace);
547
+
548
+ const lines = [];
549
+ lines.push(br('╔', '╗'));
550
+ lines.push(ln(`Dual-Brain Status — v${VERSION}`));
551
+ lines.push(sep());
552
+
553
+ lines.push(ln(`Mode: ${MODE_LABELS[mode.mode]}`));
554
+ lines.push(ln(`Profile: ${profile.name}`));
555
+ lines.push(ln(` ${PROFILES[profile.name]?.description || ''}`));
556
+ if (profile.switched_at) {
557
+ lines.push(ln(` Set: ${profile.switched_at.slice(0, 16).replace('T', ' ')}`));
558
+ }
559
+
560
+ lines.push(sep());
561
+
562
+ lines.push(ln('Budget Limits'));
563
+ lines.push(ln(` Session: warn $${profile.budgets.session_warn_usd} / limit $${profile.budgets.session_limit_usd}`));
564
+ lines.push(ln(` Daily: warn $${profile.budgets.daily_warn_usd} / limit $${profile.budgets.daily_limit_usd}`));
565
+
566
+ lines.push(sep());
567
+
568
+ lines.push(ln('Providers'));
569
+ const cAuth = env.claude.authed ? 'authenticated' : 'not authenticated';
570
+ const xAuth = env.codex.authed ? 'authenticated' : env.codex.installed ? 'not authenticated' : 'not found';
571
+ lines.push(ln(` Claude: ${statusIcon(env.claude.authed)} ${cAuth}`));
572
+ lines.push(ln(` Codex: ${statusIcon(env.codex.authed)} ${xAuth}`));
573
+
574
+ lines.push(sep());
575
+
576
+ lines.push(ln('Quality Gate'));
577
+ lines.push(ln(` Reviews from: ${profile.quality_gate.sensitivity_floor} risk+`));
578
+ lines.push(ln(` Dual-brain at: ${profile.quality_gate.dual_brain_minimum} risk+`));
579
+
580
+ const balancer = join(workspace, '.claude', 'hooks', 'budget-balancer.mjs');
581
+ if (existsSync(balancer)) {
582
+ const proc = run(process.execPath, [balancer]);
583
+ if (proc.status === 0 && proc.stdout.trim()) {
584
+ lines.push(sep());
585
+ lines.push(ln('Provider Pressure (5hr rolling)'));
586
+ for (const l of proc.stdout.trim().split('\n')) {
587
+ if (l.includes('█') || l.includes('░') || l.includes('Recommendation')) {
588
+ const cleaned = l.replace(/[║╔╗╠╣╚╝═]/g, '').trim();
589
+ if (cleaned) lines.push(ln(` ${cleaned}`));
590
+ }
591
+ }
592
+ }
593
+ }
594
+
595
+ lines.push(br('╚', '╝'));
596
+
597
+ console.log('');
598
+ for (const l of lines) console.log(` ${l}`);
599
+ console.log('');
600
+
601
+ if (IS_REPLIT) {
602
+ console.log(' Quick actions (paste into shell):');
603
+ console.log(` ${cmd('npx dual-brain mode cost-saver')} # switch profile`);
604
+ console.log(` ${cmd('npx dual-brain budget 8 25')} # set limits`);
605
+ console.log('');
606
+ }
607
+ }
608
+
609
+ // ─── Subcommand: mode ──────────────────────────────────────────────────────
610
+
611
+ function cmdMode() {
612
+ const workspace = resolve(process.cwd());
613
+ const modeArg = positional[1] || null;
614
+
615
+ if (!modeArg || modeArg === 'list') {
616
+ const current = loadProfile(workspace);
617
+ const PEMOJIS = { balanced: '⚖️ ', 'cost-saver': '💸', 'quality-first': '💎' };
618
+ console.log('');
619
+ console.log(' 🎛️ Profiles:');
620
+ console.log('');
621
+ for (const [name, p] of Object.entries(PROFILES)) {
622
+ const active = name === current.name ? ' ✅ active' : '';
623
+ console.log(` ${PEMOJIS[name] || ' '} ${name.padEnd(15)} ${p.description}${active}`);
624
+ }
625
+ console.log('');
626
+ console.log(` Switch: ${cmd('npx dual-brain mode <profile>')}`);
627
+ console.log('');
628
+ return;
629
+ }
630
+
631
+ if (!PROFILES[modeArg]) {
632
+ console.error(` Unknown profile: ${modeArg}`);
633
+ console.error(` Available: ${Object.keys(PROFILES).join(', ')}`);
634
+ process.exit(1);
635
+ }
636
+
637
+ const profile = PROFILES[modeArg];
638
+
639
+ let customOverrides = null;
640
+ try {
641
+ const existing = JSON.parse(readFileSync(profilePath(workspace), 'utf8'));
642
+ if (existing.custom_overrides?.budgets) {
643
+ customOverrides = { budgets: existing.custom_overrides.budgets };
644
+ }
645
+ } catch {}
646
+
647
+ saveProfile(workspace, modeArg, customOverrides);
648
+
649
+ const PEMOJIS = { balanced: '⚖️ ', 'cost-saver': '💸', 'quality-first': '💎' };
650
+ console.log('');
651
+ console.log(` ✅ Profile switched: ${PEMOJIS[modeArg] || ''} ${modeArg}`);
652
+ console.log(` ${profile.description}`);
653
+ console.log('');
654
+ console.log(' 🧭 Routing changes:');
655
+ console.log(` Provider: ${profile.routing.prefer_provider}`);
656
+ console.log(` 💵 Budget: $${profile.budgets.session_limit_usd}/session, $${profile.budgets.daily_limit_usd}/day`);
657
+ console.log(` 🛡️ Reviews: ${profile.quality_gate.sensitivity_floor} risk+`);
658
+ console.log(` 🧠 Dual-brain: ${profile.quality_gate.dual_brain_minimum} risk+`);
659
+ console.log('');
660
+ console.log(' 🟢 Active immediately, no restart needed.');
661
+ console.log('');
662
+ }
663
+
664
+ // ─── Subcommand: budget ────────────────────────────────────────────────────
665
+
666
+ function cmdBudget() {
667
+ const workspace = resolve(process.cwd());
668
+ const sessionArg = positional[1] ? parseFloat(positional[1]) : null;
669
+ const dailyArg = positional[2] ? parseFloat(positional[2]) : null;
670
+
671
+ if (sessionArg == null) {
672
+ const profile = loadProfile(workspace);
673
+ console.log('');
674
+ console.log(' 💵 Current budget:');
675
+ console.log(` Session: ⚠️ $${profile.budgets.session_warn_usd} warn · 🛑 $${profile.budgets.session_limit_usd} limit`);
676
+ console.log(` Daily: ⚠️ $${profile.budgets.daily_warn_usd} warn · 🛑 $${profile.budgets.daily_limit_usd} limit`);
677
+ console.log('');
678
+ console.log(` Set limits: ${cmd('npx dual-brain budget <session$> [daily$]')}`);
679
+ console.log(` Example: ${cmd('npx dual-brain budget 8 25')}`);
680
+ console.log('');
681
+ return;
682
+ }
683
+
684
+ if (isNaN(sessionArg) || sessionArg <= 0) {
685
+ console.error(' Session limit must be a positive number');
686
+ process.exit(1);
687
+ }
688
+
689
+ const daily = (dailyArg != null && !isNaN(dailyArg) && dailyArg > 0) ? dailyArg : sessionArg * 3;
690
+
691
+ let existing = {};
692
+ try { existing = JSON.parse(readFileSync(profilePath(workspace), 'utf8')); } catch {}
693
+
694
+ const customOverrides = existing.custom_overrides || {};
695
+ customOverrides.budgets = {
696
+ session_warn_usd: +(sessionArg * 0.6).toFixed(2),
697
+ session_limit_usd: sessionArg,
698
+ daily_warn_usd: +(daily * 0.6).toFixed(2),
699
+ daily_limit_usd: daily,
700
+ };
701
+
702
+ const data = {
703
+ active: existing.active || 'balanced',
704
+ switched_at: existing.switched_at || new Date().toISOString(),
705
+ custom_overrides: customOverrides,
706
+ };
707
+ const budgetTarget = profilePath(workspace);
708
+ const budgetTmp = budgetTarget + '.tmp.' + process.pid;
709
+ writeFileSync(budgetTmp, JSON.stringify(data, null, 2) + '\n');
710
+ renameSync(budgetTmp, budgetTarget);
711
+
712
+ console.log('');
713
+ console.log(' ✅ Budget updated:');
714
+ console.log(` Session: ⚠️ $${customOverrides.budgets.session_warn_usd} warn · 🛑 $${sessionArg} limit`);
715
+ console.log(` Daily: ⚠️ $${customOverrides.budgets.daily_warn_usd} warn · 🛑 $${daily} limit`);
716
+ console.log('');
717
+ console.log(' 🟢 Active immediately, no restart needed.');
718
+ console.log('');
719
+ }
720
+
721
+ // ─── Subcommand: explain ───────────────────────────────────────────────────
722
+
723
+ function cmdExplain() {
724
+ const workspace = resolve(process.cwd());
725
+ const hooksDir = join(workspace, '.claude', 'hooks');
726
+ const today = new Date().toISOString().slice(0, 10);
727
+ const logFile = join(hooksDir, `usage-${today}.jsonl`);
728
+
729
+ if (!existsSync(logFile)) {
730
+ console.log('');
731
+ console.log(' 💤 No routing decisions recorded today.');
732
+ console.log(' Start a Claude Code session and the tier enforcer will log decisions.');
733
+ console.log('');
734
+ return;
735
+ }
736
+
737
+ let lines;
738
+ try {
739
+ lines = readFileSync(logFile, 'utf8').split('\n').filter(Boolean);
740
+ } catch {
741
+ console.log(' Could not read usage log.');
742
+ return;
743
+ }
744
+
745
+ let lastRec = null;
746
+ for (let i = lines.length - 1; i >= 0; i--) {
747
+ try {
748
+ const entry = JSON.parse(lines[i]);
749
+ if (entry.type === 'tier_recommendation') { lastRec = entry; break; }
750
+ } catch {}
751
+ }
752
+
753
+ if (!lastRec) {
754
+ console.log('');
755
+ console.log(' 💤 No routing decisions found in today\'s log.');
756
+ console.log(' The tier enforcer logs decisions when Agent tool is used.');
757
+ console.log('');
758
+ return;
759
+ }
760
+
761
+ const profile = loadProfile(workspace);
762
+
763
+ console.log('');
764
+ console.log(' 🧭 Last Routing Decision');
765
+ console.log(' ' + '─'.repeat(40));
766
+ console.log(` 🕐 Time: ${lastRec.timestamp?.slice(11, 19) || 'unknown'}`);
767
+ console.log(` 🔎 Detected: ${lastRec.detected_tier || 'unknown'} tier`);
768
+ console.log(` 🧠 Recommended: ${lastRec.recommended_model || 'unknown'}`);
769
+ console.log(` 🎯 Actual: ${lastRec.actual_model || 'unknown'}`);
770
+ console.log(` ${lastRec.followed ? '✅' : '⚠️'} Followed: ${lastRec.followed ? 'yes' : 'no'}`);
771
+ console.log(` 🎛️ Profile: ${profile.name}`);
772
+ console.log('');
773
+
774
+ if (!lastRec.followed) {
775
+ console.log(' ⚠️ Recommendation was overridden. This may mean:');
776
+ console.log(' - The task needed a different model (valid override)');
777
+ console.log(' - The subagent_type forced a specific tier');
778
+ console.log(` - Profile "${profile.name}" adjusted the threshold`);
779
+ } else {
780
+ console.log(' ✅ Routing matched the recommendation.');
781
+ }
782
+
783
+ let total = 0, followed = 0;
784
+ for (const line of lines) {
785
+ try {
786
+ const e = JSON.parse(line);
787
+ if (e.type === 'tier_recommendation') { total++; if (e.followed) followed++; }
788
+ } catch {}
789
+ }
790
+ const pct = total > 0 ? Math.round((followed / total) * 100) : 0;
791
+ console.log('');
792
+ console.log(` Today: ${followed}/${total} recommendations followed (${pct}%)`);
793
+ console.log('');
794
+ }
795
+
443
796
  // ─── Main ───────────────────────────────────────────────────────────────────
444
797
 
445
798
  function main() {
799
+ if (subcommand === 'status') {
800
+ // Launch interactive TUI if available and TTY
801
+ const panelPath = join(resolve(process.cwd()), '.claude', 'hooks', 'control-panel.mjs');
802
+ const pkgPanel = join(__dirname, 'hooks', 'control-panel.mjs');
803
+ const panel = existsSync(panelPath) ? panelPath : existsSync(pkgPanel) ? pkgPanel : null;
804
+ if (panel && process.stdin.isTTY && process.stdout.isTTY && !process.env.CI) {
805
+ const { status } = spawnSync(process.execPath, [panel], { stdio: 'inherit' });
806
+ process.exit(status || 0);
807
+ }
808
+ cmdStatus();
809
+ return;
810
+ }
811
+ if (subcommand === 'mode') { cmdMode(); return; }
812
+ if (subcommand === 'budget') { cmdBudget(); return; }
813
+ if (subcommand === 'explain') { cmdExplain(); return; }
814
+
446
815
  const env = detectEnvironment();
447
816
  const mode = resolveMode(env);
448
817
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dual-brain",
3
- "version": "3.1.0",
3
+ "version": "3.3.0",
4
4
  "description": "Dual-provider orchestration for Claude Code — tiered routing, budget balancing, and GPT dual-brain review across Claude + OpenAI subscriptions",
5
5
  "type": "module",
6
6
  "bin": {
@@ -27,7 +27,7 @@
27
27
  },
28
28
  "files": [
29
29
  "install.mjs",
30
- "hooks/",
30
+ "hooks/*.mjs",
31
31
  "orchestrator.json",
32
32
  "hookify.*.local.md",
33
33
  "review-rules.md",
@@ -1,5 +0,0 @@
1
- {"timestamp":"2026-05-14T00:33:17.462Z","type":"tier_recommendation","detected_tier":"search","recommended_model":"haiku","actual_model":"opus","prompt_hash":"9990d6fd8943","followed":false}
2
- {"timestamp":"2026-05-14T00:33:17.501Z","type":"tier_recommendation","detected_tier":"execute","recommended_model":"sonnet","actual_model":"sonnet","prompt_hash":"c636a5e74cdc","followed":true}
3
- {"timestamp":"2026-05-14T00:33:17.536Z","type":"tier_recommendation","detected_tier":"think","recommended_model":"opus","actual_model":"haiku","prompt_hash":"bd666c33402c","followed":false}
4
- {"timestamp":"2026-05-14T00:33:17.606Z","type":"tier_recommendation","detected_tier":"execute","recommended_model":"sonnet","actual_model":"unknown-model-xyz","prompt_hash":"913203af69f8","followed":false}
5
- {"timestamp":"2026-05-14T00:33:17.746Z","type":"tier_recommendation","detected_tier":"think","recommended_model":"opus","actual_model":"gpt-4.1-mini","prompt_hash":"7196103e6569","followed":false}