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/CLAUDE.md +33 -1
- package/hooks/budget-balancer.mjs +45 -6
- package/hooks/control-panel.mjs +489 -0
- package/hooks/cost-logger.mjs +51 -26
- package/hooks/decision-ledger.mjs +299 -0
- package/hooks/dual-brain-review.mjs +106 -17
- package/hooks/dual-brain-think.mjs +81 -17
- package/hooks/enforce-tier.mjs +103 -10
- package/hooks/gpt-work-dispatcher.mjs +50 -6
- package/hooks/profiles.mjs +203 -0
- package/hooks/quality-gate.mjs +34 -6
- package/hooks/summary-checkpoint.mjs +231 -0
- package/install.mjs +402 -33
- package/package.json +2 -2
- package/hooks/usage-2026-05-14.jsonl +0 -5
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
|
-
|
|
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
|
|
40
|
-
--dry-run Detect environment only
|
|
41
|
-
--json Output detection as JSON
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
console.error(`
|
|
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(
|
|
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:
|
|
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 ? '
|
|
371
|
-
lines.push(ln(` Claude
|
|
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 ? '
|
|
375
|
-
lines.push(ln(` Codex
|
|
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(
|
|
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('
|
|
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
|
|
409
|
-
: 'Ready
|
|
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
|
|
421
|
-
console.log(' Every Claude Code session
|
|
422
|
-
console.log('
|
|
423
|
-
console.log('
|
|
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('
|
|
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/
|
|
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.
|
|
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}
|