dual-brain 3.0.1 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +13 -0
- package/hooks/budget-balancer.mjs +45 -6
- package/hooks/cost-logger.mjs +51 -26
- package/hooks/cost-report.mjs +2 -2
- package/hooks/decision-ledger.mjs +299 -0
- package/hooks/dual-brain-review.mjs +1 -1
- 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/setup-wizard.mjs +1 -1
- package/hooks/summary-checkpoint.mjs +231 -0
- package/install.mjs +387 -11
- package/package.json +2 -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, 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,27 +29,53 @@ 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;
|
|
34
|
+
|
|
35
|
+
if (flag('--version') || flag('-v')) {
|
|
36
|
+
console.log(`dual-brain v${VERSION}`);
|
|
37
|
+
process.exit(0);
|
|
38
|
+
}
|
|
26
39
|
|
|
27
40
|
if (flag('--help') || flag('-h')) {
|
|
28
41
|
console.log(`
|
|
29
42
|
dual-brain v${VERSION} — Dual-provider orchestrator for Claude Code
|
|
30
43
|
|
|
31
|
-
Usage: npx -y dual-brain [options]
|
|
44
|
+
Usage: npx -y dual-brain [command] [options]
|
|
45
|
+
|
|
46
|
+
Commands:
|
|
47
|
+
(none) Auto-detect and install/update orchestrator
|
|
48
|
+
status Live view of mode, spend, pressure, profile
|
|
49
|
+
mode Show or switch profile (balanced, cost-saver, quality-first)
|
|
50
|
+
budget Set session/daily spend limits
|
|
51
|
+
explain Show why the last routing decision was made
|
|
52
|
+
init Alias for default install (backward compat)
|
|
32
53
|
|
|
33
54
|
Options:
|
|
34
55
|
--force Overwrite all existing config (keeps review-rules.md)
|
|
35
56
|
--dry-run Detect environment only, don't install
|
|
36
57
|
--json Output detection as JSON (implies --dry-run)
|
|
37
58
|
--help Show this help
|
|
59
|
+
|
|
60
|
+
Profiles:
|
|
61
|
+
balanced Standard routing — best model for each tier
|
|
62
|
+
cost-saver Minimize spend — prefer cheaper models
|
|
63
|
+
quality-first Maximum quality — dual-brain for medium+ risk
|
|
64
|
+
|
|
65
|
+
Examples:
|
|
66
|
+
${cmd('npx dual-brain')} # install or update
|
|
67
|
+
${cmd('npx dual-brain status')} # live dashboard
|
|
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
|
|
38
71
|
`);
|
|
39
72
|
process.exit(0);
|
|
40
73
|
}
|
|
41
74
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
console.error(`
|
|
46
|
-
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')}`);
|
|
47
79
|
process.exit(1);
|
|
48
80
|
}
|
|
49
81
|
|
|
@@ -240,7 +272,20 @@ function generateSettings(workspace) {
|
|
|
240
272
|
],
|
|
241
273
|
};
|
|
242
274
|
|
|
243
|
-
|
|
275
|
+
const DUAL_BRAIN_CMDS = [
|
|
276
|
+
'node .claude/hooks/enforce-tier.mjs',
|
|
277
|
+
'node .claude/hooks/cost-logger.mjs',
|
|
278
|
+
];
|
|
279
|
+
|
|
280
|
+
const merged = { ...(existing.hooks || {}) };
|
|
281
|
+
for (const [event, entries] of Object.entries(hooks)) {
|
|
282
|
+
const existingEntries = (merged[event] || []).filter(e =>
|
|
283
|
+
!e.hooks?.some(h => DUAL_BRAIN_CMDS.includes(h.command))
|
|
284
|
+
);
|
|
285
|
+
merged[event] = [...existingEntries, ...entries];
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return { ...existing, hooks: merged };
|
|
244
289
|
}
|
|
245
290
|
|
|
246
291
|
function generateClaudeMd(mode) {
|
|
@@ -265,6 +310,9 @@ function generateGitignoreEntries(workspace) {
|
|
|
265
310
|
'.claude/reviews/',
|
|
266
311
|
'.claude/hooks/.drift-warned',
|
|
267
312
|
'.claude/hooks/.budget-alerted',
|
|
313
|
+
'.claude/dual-brain.profile.json',
|
|
314
|
+
'.claude/hooks/usage-summary-*.json',
|
|
315
|
+
'.claude/hooks/decision-ledger.jsonl',
|
|
268
316
|
];
|
|
269
317
|
let existing = '';
|
|
270
318
|
try { existing = readFileSync(join(workspace, '.gitignore'), 'utf8'); } catch {}
|
|
@@ -285,7 +333,8 @@ function install(workspace, env, mode) {
|
|
|
285
333
|
'dual-brain-review.mjs', 'dual-brain-think.mjs', 'quality-gate.mjs',
|
|
286
334
|
'test-orchestrator.mjs', 'setup-wizard.mjs', 'health-check.mjs',
|
|
287
335
|
'install-git-hooks.mjs', 'session-report.mjs', 'budget-balancer.mjs',
|
|
288
|
-
'gpt-work-dispatcher.mjs',
|
|
336
|
+
'gpt-work-dispatcher.mjs', 'profiles.mjs',
|
|
337
|
+
'summary-checkpoint.mjs', 'decision-ledger.mjs',
|
|
289
338
|
];
|
|
290
339
|
for (const h of HOOKS) cpSync(join(__dirname, 'hooks', h), join(target, 'hooks', h));
|
|
291
340
|
actions.push(`✓ ${HOOKS.length} hook scripts`);
|
|
@@ -322,7 +371,7 @@ function install(workspace, env, mode) {
|
|
|
322
371
|
if (needed.length > 0) {
|
|
323
372
|
writeFileSync(
|
|
324
373
|
join(workspace, '.gitignore'),
|
|
325
|
-
gi + '\n# Dual-Brain Orchestrator\n' + needed.join('\n') + '\n'
|
|
374
|
+
(gi && !gi.endsWith('\n') ? gi + '\n' : gi) + '\n# Dual-Brain Orchestrator\n' + needed.join('\n') + '\n'
|
|
326
375
|
);
|
|
327
376
|
actions.push('✓ .gitignore updated');
|
|
328
377
|
}
|
|
@@ -407,7 +456,19 @@ function printReport(env, mode, actions) {
|
|
|
407
456
|
console.log(' Both Claude and GPT are available as work providers.');
|
|
408
457
|
}
|
|
409
458
|
console.log('');
|
|
410
|
-
|
|
459
|
+
if (IS_REPLIT) {
|
|
460
|
+
console.log(' Try these in your Replit shell (paste with ! prefix):');
|
|
461
|
+
console.log(` ${cmd('npx dual-brain status')} # live dashboard`);
|
|
462
|
+
console.log(` ${cmd('npx dual-brain mode cost-saver')} # switch profile`);
|
|
463
|
+
console.log(` ${cmd('npx dual-brain budget 8 25')} # set limits`);
|
|
464
|
+
} else {
|
|
465
|
+
console.log(' Try these in your next Claude Code session:');
|
|
466
|
+
console.log(' npx dual-brain status # live dashboard');
|
|
467
|
+
console.log(' npx dual-brain mode cost-saver # switch profile');
|
|
468
|
+
console.log(' npx dual-brain budget 8 25 # set limits');
|
|
469
|
+
}
|
|
470
|
+
console.log('');
|
|
471
|
+
console.log(' In-session tools (ask Claude to run these):');
|
|
411
472
|
console.log(' node .claude/hooks/health-check.mjs # verify setup');
|
|
412
473
|
console.log(' node .claude/hooks/cost-report.mjs # see activity');
|
|
413
474
|
console.log(' node .claude/hooks/budget-balancer.mjs # provider balance');
|
|
@@ -422,9 +483,324 @@ function printReport(env, mode, actions) {
|
|
|
422
483
|
}
|
|
423
484
|
}
|
|
424
485
|
|
|
486
|
+
// ─── Profile System ────────────────────────────────────────────────────────
|
|
487
|
+
|
|
488
|
+
const PROFILE_FILE_REL = '.claude/dual-brain.profile.json';
|
|
489
|
+
|
|
490
|
+
function profilePath(workspace) {
|
|
491
|
+
return join(workspace || process.cwd(), PROFILE_FILE_REL);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const PROFILES = {
|
|
495
|
+
balanced: {
|
|
496
|
+
description: 'Standard routing — best model for each tier, normal budgets',
|
|
497
|
+
routing: { prefer_provider: 'auto', think_threshold: 'normal', gpt_dispatch_bias: 0 },
|
|
498
|
+
budgets: { session_warn_usd: 5, session_limit_usd: 10, daily_warn_usd: 20, daily_limit_usd: 50 },
|
|
499
|
+
quality_gate: { sensitivity_floor: 'medium', dual_brain_minimum: 'high' },
|
|
500
|
+
},
|
|
501
|
+
'cost-saver': {
|
|
502
|
+
description: 'Minimize spend — prefer cheaper models, skip GPT for low risk',
|
|
503
|
+
routing: { prefer_provider: 'cheapest', think_threshold: 'strict', gpt_dispatch_bias: -20 },
|
|
504
|
+
budgets: { session_warn_usd: 2, session_limit_usd: 5, daily_warn_usd: 8, daily_limit_usd: 20 },
|
|
505
|
+
quality_gate: { sensitivity_floor: 'high', dual_brain_minimum: 'critical' },
|
|
506
|
+
},
|
|
507
|
+
'quality-first': {
|
|
508
|
+
description: 'Maximum quality — dual-brain for medium+, stricter reviews',
|
|
509
|
+
routing: { prefer_provider: 'most-capable', think_threshold: 'relaxed', gpt_dispatch_bias: 10 },
|
|
510
|
+
budgets: { session_warn_usd: 15, session_limit_usd: 30, daily_warn_usd: 50, daily_limit_usd: 100 },
|
|
511
|
+
quality_gate: { sensitivity_floor: 'low', dual_brain_minimum: 'medium' },
|
|
512
|
+
},
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
function loadProfile(workspace) {
|
|
516
|
+
try {
|
|
517
|
+
const data = JSON.parse(readFileSync(profilePath(workspace), 'utf8'));
|
|
518
|
+
const name = data.active && PROFILES[data.active] ? data.active : 'balanced';
|
|
519
|
+
const profile = PROFILES[name];
|
|
520
|
+
const custom = data.custom_overrides || {};
|
|
521
|
+
return {
|
|
522
|
+
name,
|
|
523
|
+
...profile,
|
|
524
|
+
budgets: { ...profile.budgets, ...custom.budgets },
|
|
525
|
+
routing: { ...profile.routing, ...custom.routing },
|
|
526
|
+
switched_at: data.switched_at || null,
|
|
527
|
+
};
|
|
528
|
+
} catch {
|
|
529
|
+
return { name: 'balanced', ...PROFILES.balanced, switched_at: null };
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function saveProfile(workspace, name, customOverrides) {
|
|
534
|
+
const data = { active: name, switched_at: new Date().toISOString() };
|
|
535
|
+
if (customOverrides) data.custom_overrides = customOverrides;
|
|
536
|
+
const target = profilePath(workspace);
|
|
537
|
+
const tmp = target + '.tmp.' + process.pid;
|
|
538
|
+
writeFileSync(tmp, JSON.stringify(data, null, 2) + '\n');
|
|
539
|
+
renameSync(tmp, target);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// ─── Subcommand: status ────────────────────────────────────────────────────
|
|
543
|
+
|
|
544
|
+
function cmdStatus() {
|
|
545
|
+
const workspace = resolve(process.cwd());
|
|
546
|
+
const env = detectEnvironment();
|
|
547
|
+
const mode = resolveMode(env);
|
|
548
|
+
const profile = loadProfile(workspace);
|
|
549
|
+
|
|
550
|
+
const lines = [];
|
|
551
|
+
lines.push(br('╔', '╗'));
|
|
552
|
+
lines.push(ln(`Dual-Brain Status — v${VERSION}`));
|
|
553
|
+
lines.push(sep());
|
|
554
|
+
|
|
555
|
+
lines.push(ln(`Mode: ${MODE_LABELS[mode.mode]}`));
|
|
556
|
+
lines.push(ln(`Profile: ${profile.name}`));
|
|
557
|
+
lines.push(ln(` ${PROFILES[profile.name]?.description || ''}`));
|
|
558
|
+
if (profile.switched_at) {
|
|
559
|
+
lines.push(ln(` Set: ${profile.switched_at.slice(0, 16).replace('T', ' ')}`));
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
lines.push(sep());
|
|
563
|
+
|
|
564
|
+
lines.push(ln('Budget Limits'));
|
|
565
|
+
lines.push(ln(` Session: warn $${profile.budgets.session_warn_usd} / limit $${profile.budgets.session_limit_usd}`));
|
|
566
|
+
lines.push(ln(` Daily: warn $${profile.budgets.daily_warn_usd} / limit $${profile.budgets.daily_limit_usd}`));
|
|
567
|
+
|
|
568
|
+
lines.push(sep());
|
|
569
|
+
|
|
570
|
+
lines.push(ln('Providers'));
|
|
571
|
+
const cAuth = env.claude.authed ? 'authenticated' : 'not authenticated';
|
|
572
|
+
const xAuth = env.codex.authed ? 'authenticated' : env.codex.installed ? 'not authenticated' : 'not found';
|
|
573
|
+
lines.push(ln(` Claude: ${statusIcon(env.claude.authed)} ${cAuth}`));
|
|
574
|
+
lines.push(ln(` Codex: ${statusIcon(env.codex.authed)} ${xAuth}`));
|
|
575
|
+
|
|
576
|
+
lines.push(sep());
|
|
577
|
+
|
|
578
|
+
lines.push(ln('Quality Gate'));
|
|
579
|
+
lines.push(ln(` Reviews from: ${profile.quality_gate.sensitivity_floor} risk+`));
|
|
580
|
+
lines.push(ln(` Dual-brain at: ${profile.quality_gate.dual_brain_minimum} risk+`));
|
|
581
|
+
|
|
582
|
+
const balancer = join(workspace, '.claude', 'hooks', 'budget-balancer.mjs');
|
|
583
|
+
if (existsSync(balancer)) {
|
|
584
|
+
const proc = run(process.execPath, [balancer]);
|
|
585
|
+
if (proc.status === 0 && proc.stdout.trim()) {
|
|
586
|
+
lines.push(sep());
|
|
587
|
+
lines.push(ln('Provider Pressure (5hr rolling)'));
|
|
588
|
+
for (const l of proc.stdout.trim().split('\n')) {
|
|
589
|
+
if (l.includes('█') || l.includes('░') || l.includes('Recommendation')) {
|
|
590
|
+
const cleaned = l.replace(/[║╔╗╠╣╚╝═]/g, '').trim();
|
|
591
|
+
if (cleaned) lines.push(ln(` ${cleaned}`));
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
lines.push(br('╚', '╝'));
|
|
598
|
+
|
|
599
|
+
console.log('');
|
|
600
|
+
for (const l of lines) console.log(` ${l}`);
|
|
601
|
+
console.log('');
|
|
602
|
+
|
|
603
|
+
if (IS_REPLIT) {
|
|
604
|
+
console.log(' Quick actions (paste into shell):');
|
|
605
|
+
console.log(` ${cmd('npx dual-brain mode cost-saver')} # switch profile`);
|
|
606
|
+
console.log(` ${cmd('npx dual-brain budget 8 25')} # set limits`);
|
|
607
|
+
console.log('');
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// ─── Subcommand: mode ──────────────────────────────────────────────────────
|
|
612
|
+
|
|
613
|
+
function cmdMode() {
|
|
614
|
+
const workspace = resolve(process.cwd());
|
|
615
|
+
const modeArg = positional[1] || null;
|
|
616
|
+
|
|
617
|
+
if (!modeArg || modeArg === 'list') {
|
|
618
|
+
const current = loadProfile(workspace);
|
|
619
|
+
console.log('');
|
|
620
|
+
console.log(' Available profiles:');
|
|
621
|
+
console.log('');
|
|
622
|
+
for (const [name, p] of Object.entries(PROFILES)) {
|
|
623
|
+
const active = name === current.name ? ' ← active' : '';
|
|
624
|
+
console.log(` ${name.padEnd(15)} ${p.description}${active}`);
|
|
625
|
+
}
|
|
626
|
+
console.log('');
|
|
627
|
+
console.log(` Switch: ${cmd('npx dual-brain mode <profile>')}`);
|
|
628
|
+
console.log('');
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
if (!PROFILES[modeArg]) {
|
|
633
|
+
console.error(` Unknown profile: ${modeArg}`);
|
|
634
|
+
console.error(` Available: ${Object.keys(PROFILES).join(', ')}`);
|
|
635
|
+
process.exit(1);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const profile = PROFILES[modeArg];
|
|
639
|
+
|
|
640
|
+
let customOverrides = null;
|
|
641
|
+
try {
|
|
642
|
+
const existing = JSON.parse(readFileSync(profilePath(workspace), 'utf8'));
|
|
643
|
+
if (existing.custom_overrides?.budgets) {
|
|
644
|
+
customOverrides = { budgets: existing.custom_overrides.budgets };
|
|
645
|
+
}
|
|
646
|
+
} catch {}
|
|
647
|
+
|
|
648
|
+
saveProfile(workspace, modeArg, customOverrides);
|
|
649
|
+
|
|
650
|
+
console.log('');
|
|
651
|
+
console.log(` Profile switched to: ${modeArg}`);
|
|
652
|
+
console.log(` ${profile.description}`);
|
|
653
|
+
console.log('');
|
|
654
|
+
console.log(' What changed:');
|
|
655
|
+
console.log(` Routing: ${profile.routing.prefer_provider}`);
|
|
656
|
+
console.log(` Budget: $${profile.budgets.session_limit_usd}/session, $${profile.budgets.daily_limit_usd}/day`);
|
|
657
|
+
console.log(` Reviews from: ${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 limits:');
|
|
675
|
+
console.log(` Session: warn $${profile.budgets.session_warn_usd} / limit $${profile.budgets.session_limit_usd}`);
|
|
676
|
+
console.log(` Daily: warn $${profile.budgets.daily_warn_usd} / limit $${profile.budgets.daily_limit_usd}`);
|
|
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 limits updated:');
|
|
714
|
+
console.log(` Session: warn $${customOverrides.budgets.session_warn_usd} / limit $${sessionArg}`);
|
|
715
|
+
console.log(` Daily: warn $${customOverrides.budgets.daily_warn_usd} / limit $${daily}`);
|
|
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(` Followed: ${lastRec.followed ? 'yes' : 'no'}`);
|
|
771
|
+
console.log(` Profile: ${profile.name}`);
|
|
772
|
+
console.log('');
|
|
773
|
+
|
|
774
|
+
if (!lastRec.followed) {
|
|
775
|
+
console.log(' The recommendation was not followed. 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(' The recommendation was followed — routing worked as expected.');
|
|
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
|
+
|
|
425
796
|
// ─── Main ───────────────────────────────────────────────────────────────────
|
|
426
797
|
|
|
427
798
|
function main() {
|
|
799
|
+
if (subcommand === 'status') { cmdStatus(); return; }
|
|
800
|
+
if (subcommand === 'mode') { cmdMode(); return; }
|
|
801
|
+
if (subcommand === 'budget') { cmdBudget(); return; }
|
|
802
|
+
if (subcommand === 'explain') { cmdExplain(); return; }
|
|
803
|
+
|
|
428
804
|
const env = detectEnvironment();
|
|
429
805
|
const mode = resolveMode(env);
|
|
430
806
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dual-brain",
|
|
3
|
-
"version": "3.0
|
|
3
|
+
"version": "3.2.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",
|