dual-brain 3.7.2 → 3.8.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Dual-Brain Orchestrator
2
2
 
3
- One command. Both brains. Auto-detected. Auto-configured.
3
+ One command. Both brains. Auto-detected. Auto-configured. Default profile: **auto**.
4
4
 
5
5
  Dual-provider orchestration for Claude Code across Claude and OpenAI subscriptions. Routes search to cheap models, execution to mid-tier, thinking to the most capable. Dispatches work to GPT via Codex CLI. Dual-brain analysis for high-risk decisions.
6
6
 
@@ -36,9 +36,9 @@ npx -y dual-brain
36
36
 
37
37
  ## How it works
38
38
 
39
- **Two hooks fire automatically** (registered in `.claude/settings.json`):
39
+ **Two advisory hooks** are registered in `.claude/settings.json` and fire on each tool use. They detect and recommend — they do not execute actions without user confirmation:
40
40
 
41
- - **enforce-tier.mjs** (PreToolUse on Agent): Classifies tasks, advises the correct model, detects duplicates, suggests cross-provider routing
41
+ - **enforce-tier.mjs** (PreToolUse on Agent): Classifies tasks, recommends the correct model tier, detects duplicates, suggests cross-provider routing
42
42
  - **cost-logger.mjs** (PostToolUse on all tools): Logs usage to daily rotated files for cost tracking
43
43
 
44
44
  **Three tiers route work by complexity:**
@@ -49,7 +49,7 @@ npx -y dual-brain
49
49
  | Execute | Sonnet | GPT-5.4 | edits, tests, git ops |
50
50
  | Think | Opus | GPT-5.5 | architecture, review, planning |
51
51
 
52
- **Dual-brain** kicks in automatically for high-risk decisions — both providers think on the same problem independently.
52
+ **Dual-brain** is recommended automatically for high-risk decisions — hooks detect the risk level and suggest dual-brain analysis, where both providers think on the same problem independently.
53
53
 
54
54
  ## Scripts
55
55
 
@@ -63,7 +63,7 @@ npx -y dual-brain
63
63
  | `hooks/gpt-work-dispatcher.mjs` | Dispatch execution tasks to GPT via Codex CLI |
64
64
  | `hooks/session-report.mjs` | Session-end summary: activity, compliance, quality |
65
65
  | `hooks/health-check.mjs` | Verify all hooks and dependencies are working |
66
- | `hooks/test-orchestrator.mjs` | Self-test harness (14 tests) |
66
+ | `hooks/test-orchestrator.mjs` | Self-test harness (29 tests) |
67
67
  | `hooks/setup-wizard.mjs` | Interactive config (optional — for custom plans) |
68
68
  | `hooks/install-git-hooks.mjs` | Git pre-commit hook for quality gate |
69
69
 
@@ -85,6 +85,20 @@ After install, edit these files:
85
85
  - `review-rules.md` — project-specific rules for GPT code review
86
86
  - `settings.json` — hook registrations (auto-generated, safe to extend)
87
87
 
88
+ ## Profiles
89
+
90
+ The active profile controls routing posture, budgets, and quality gate behavior. Default: **auto**.
91
+
92
+ ```bash
93
+ npx dual-brain mode cost-saver # switch profile
94
+ npx dual-brain status # check current profile and provider health
95
+ ```
96
+
97
+ - **auto** (default): Adapts routing based on task risk, provider health, and outcomes. Auto-escalates tier on repeated failures.
98
+ - **balanced**: Best model per tier, normal budgets, reviews at medium+ risk.
99
+ - **cost-saver**: Prefer cheaper models, lower budgets, skip GPT for non-critical work.
100
+ - **quality-first**: Dual-brain for medium+ risk, higher budgets, stricter reviews.
101
+
88
102
  ## Requirements
89
103
 
90
104
  - Node 20+
@@ -10,6 +10,17 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
10
10
  const CONFIG_FILE = resolve(__dirname, '..', 'orchestrator.json');
11
11
  const PROFILE_FILE = resolve(__dirname, '..', 'dual-brain.profile.json');
12
12
  const DRIFT_STATE = resolve(__dirname, '.drift-warned');
13
+ const BURST_FILE = resolve(__dirname, '.burst-state');
14
+
15
+ function detectBurst() {
16
+ const now = Date.now();
17
+ let state = { count: 0, window_start: now };
18
+ try { state = JSON.parse(readFileSync(BURST_FILE, 'utf8')); } catch {}
19
+ if (now - state.window_start > 90_000) state = { count: 0, window_start: now };
20
+ state.count++;
21
+ try { writeFileSync(BURST_FILE, JSON.stringify(state)); } catch {}
22
+ return state.count >= 3;
23
+ }
13
24
 
14
25
  function loadProfile() {
15
26
  try {
@@ -205,12 +216,23 @@ try {
205
216
  // Compute prompt hash early for duplicate detection and logging
206
217
  const promptHash = createHash('sha256').update(text).digest('hex').slice(0, 12);
207
218
 
219
+ // Burst detection — suppress noise during wave launches (3+ agents in 90s)
220
+ const burstMode = detectBurst();
221
+
208
222
  // Check for duplicate agent dispatch before tier classification
209
223
  const duplicate = checkDuplicate(promptHash);
210
224
  let duplicateWarning = null;
211
225
  if (duplicate) {
212
226
  const minutesAgo = Math.round((Date.now() - Date.parse(duplicate.timestamp)) / 60000);
213
- duplicateWarning = `**[Duplicate Warning]** A similar agent task was dispatched ${minutesAgo} minute${minutesAgo !== 1 ? 's' : ''} ago. Reuse the prior result unless the scope changed.`;
227
+ if (burstMode) {
228
+ // In burst mode, only warn on exact hash matches (same description+prompt)
229
+ if (duplicate.prompt_hash === promptHash) {
230
+ duplicateWarning = `**[Wave] [Duplicate Warning]** A similar agent task was dispatched ${minutesAgo} minute${minutesAgo !== 1 ? 's' : ''} ago. Reuse the prior result unless the scope changed.`;
231
+ }
232
+ // Otherwise suppress — similar-but-different agents in a wave are expected
233
+ } else {
234
+ duplicateWarning = `**[Duplicate Warning]** A similar agent task was dispatched ${minutesAgo} minute${minutesAgo !== 1 ? 's' : ''} ago. Reuse the prior result unless the scope changed.`;
235
+ }
214
236
  }
215
237
 
216
238
  let config;
@@ -315,7 +337,8 @@ try {
315
337
  }
316
338
 
317
339
  // Compute balance hint now that tier is resolved
318
- {
340
+ // In burst mode, skip balance hints — one hint per wave is enough
341
+ if (!burstMode) {
319
342
  const currentProvider = detectProvider(currentModel);
320
343
  if (currentProvider === 'claude') {
321
344
  const balance = quickPressureCheck(tier);
@@ -27,6 +27,7 @@ const COST_LOGGER = resolve(HOOKS, 'cost-logger.mjs');
27
27
  const DUAL_BRAIN = resolve(HOOKS, 'dual-brain-review.mjs');
28
28
  const ORCHESTRATOR = resolve(HOOKS, '..', 'orchestrator.json');
29
29
  const USAGE_JSONL = resolve(HOOKS, `usage-${new Date().toISOString().slice(0, 10)}.jsonl`);
30
+ const BURST_FILE = resolve(HOOKS, '.burst-state');
30
31
 
31
32
  // ─── Helpers ─────────────────────────────────────────────────────────────────
32
33
 
@@ -649,6 +650,84 @@ test('adaptive: cost-logger records Agent errors', () => {
649
650
  }
650
651
  });
651
652
 
653
+ // ─── Test 30: enforce-tier: burst detection activates on 3+ agents ─────────
654
+ test('enforce-tier: burst detection activates on 3+ agents', () => {
655
+ try {
656
+ // Write burst state at count 2, within window
657
+ writeFileSync(BURST_FILE, JSON.stringify({ count: 2, window_start: Date.now() }));
658
+ const payload = JSON.stringify({
659
+ tool_name: 'Agent',
660
+ tool_input: { prompt: `burst activation test ${Date.now()}`, model: 'sonnet' },
661
+ });
662
+ const { parsed, status } = run(ENFORCE_TIER, payload);
663
+ if (status !== 0) return `non-zero exit: ${status}`;
664
+ if (!parsed) return 'no valid JSON output';
665
+
666
+ // Read burst state — count should have incremented to >= 3
667
+ if (!existsSync(BURST_FILE)) return '.burst-state file was removed unexpectedly';
668
+ let state;
669
+ try { state = JSON.parse(readFileSync(BURST_FILE, 'utf8')); } catch (e) { return `.burst-state not valid JSON: ${e.message}`; }
670
+ if (state.count < 3) return `expected count >= 3, got: ${state.count}`;
671
+ return true;
672
+ } finally {
673
+ try { unlinkSync(BURST_FILE); } catch {}
674
+ }
675
+ });
676
+
677
+ // ─── Test 31: enforce-tier: burst mode suppresses duplicate warnings ───────
678
+ test('enforce-tier: burst mode suppresses duplicate warnings', () => {
679
+ try {
680
+ // Pre-set burst mode (count=5, active window)
681
+ writeFileSync(BURST_FILE, JSON.stringify({ count: 5, window_start: Date.now() }));
682
+ const payload = JSON.stringify({
683
+ tool_name: 'Agent',
684
+ tool_input: { prompt: 'burst duplicate test identical prompt', model: 'sonnet' },
685
+ });
686
+
687
+ // First call — establishes the prompt hash
688
+ run(ENFORCE_TIER, payload);
689
+ // Second identical call — in burst mode, duplicate warning should be suppressed or [Wave]-prefixed
690
+ const { parsed, status } = run(ENFORCE_TIER, payload);
691
+ if (status !== 0) return `non-zero exit: ${status}`;
692
+ if (!parsed) return 'no valid JSON output';
693
+
694
+ // In burst mode: either no duplicate warning at all, or a [Wave]-prefixed one
695
+ const msg = parsed.systemMessage || '';
696
+ const hasDuplicateWarning = msg.toLowerCase().includes('duplicate');
697
+ if (hasDuplicateWarning && !msg.includes('[Wave]'))
698
+ return `expected no duplicate warning or [Wave]-prefixed in burst mode, got: ${msg}`;
699
+ return true;
700
+ } finally {
701
+ try { unlinkSync(BURST_FILE); } catch {}
702
+ }
703
+ });
704
+
705
+ // ─── Test 32: enforce-tier: non-burst mode still warns on duplicates ───────
706
+ test('enforce-tier: non-burst mode still warns on duplicates', () => {
707
+ try {
708
+ // Expire burst state by setting window_start to 0 (well outside 90s window)
709
+ writeFileSync(BURST_FILE, JSON.stringify({ count: 0, window_start: 0 }));
710
+ const payload = JSON.stringify({
711
+ tool_name: 'Agent',
712
+ tool_input: { prompt: 'non-burst duplicate test identical prompt', model: 'sonnet' },
713
+ });
714
+
715
+ // First call — establishes the prompt hash
716
+ run(ENFORCE_TIER, payload);
717
+ // Second identical call — should trigger duplicate warning
718
+ const { parsed, status } = run(ENFORCE_TIER, payload);
719
+ if (status !== 0) return `non-zero exit: ${status}`;
720
+ if (!parsed) return 'no valid JSON output';
721
+
722
+ const msg = parsed.systemMessage || '';
723
+ if (!msg.toLowerCase().includes('duplicate'))
724
+ return `expected duplicate warning in non-burst mode, got: ${msg || '(empty)'}`;
725
+ return true;
726
+ } finally {
727
+ try { unlinkSync(BURST_FILE); } catch {}
728
+ }
729
+ });
730
+
652
731
  // ─── Summary ─────────────────────────────────────────────────────────────────
653
732
  const total = passed + failed;
654
733
  console.log(`\n${passed}/${total} tests passed`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dual-brain",
3
- "version": "3.7.2",
3
+ "version": "3.8.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": {
@@ -22,6 +22,9 @@
22
22
  "type": "git",
23
23
  "url": "https://github.com/1xmint/dual-brain.git"
24
24
  },
25
+ "scripts": {
26
+ "test": "node hooks/test-orchestrator.mjs"
27
+ },
25
28
  "engines": {
26
29
  "node": ">=20.0.0"
27
30
  },