dual-brain 5.0.1 → 6.0.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 CHANGED
@@ -98,6 +98,19 @@ Auto mode classifies risk from file paths and adjusts routing in real-time:
98
98
 
99
99
  Casual natural language → structured work. The vibe coding system translates informal requests into properly routed, risk-classified, quality-gated work.
100
100
 
101
+ **Wave Orchestrator** — the primary way to run multi-step work:
102
+ ```bash
103
+ node .claude/hooks/wave-orchestrator.mjs "fix the login bug and update the nav"
104
+ node .claude/hooks/wave-orchestrator.mjs --dry-run "refactor auth module"
105
+ node .claude/hooks/wave-orchestrator.mjs --resume <manifestId>
106
+ node .claude/hooks/wave-orchestrator.mjs --show <manifestId>
107
+ ```
108
+ The wave orchestrator decomposes intent, plans dependency-aware waves, assigns file ownership to prevent conflicts, dispatches agents with transparent routing tables, checkpoints between waves, and supports resume on failure. Every dispatch shows: provider, model, tier, effort, agent type, and routing reason.
109
+
110
+ Manifests persist to `.dualbrain/manifests/`, checkpoints to `.dualbrain/checkpoints/`.
111
+
112
+ Also available via the control panel: `[w]` Vibe workflow.
113
+
101
114
  **Intent compiler** — decompose multi-task requests:
102
115
  ```bash
103
116
  node .claude/hooks/vibe-router.mjs "fix the login bug and also update the nav"
@@ -119,13 +132,31 @@ node .claude/hooks/vibe-memory.mjs --infer # preference sug
119
132
  ```
120
133
  Tracks preferred profile, risk tolerance, active threads, and learns from usage patterns.
121
134
 
135
+ ## Budget Balancer (Token-Based)
136
+
137
+ The budget balancer tracks real token usage against actual subscription limits.
138
+
139
+ **Subscription tiers** (configured in `orchestrator.json` → `subscriptions.*.plan`):
140
+ - Claude: Pro $20, Max x5 $100, Max x20 $200
141
+ - ChatGPT: Plus $20, Pro $100, Pro $200
142
+
143
+ **Two rolling windows**: 5-hour and 7-day weekly. The higher pressure is the binding constraint.
144
+
145
+ **Token tracking**: Uses actual `input_tokens + output_tokens` from usage logs when available, falls back to conservative estimates only when logs lack token data.
146
+
147
+ ```bash
148
+ node .claude/hooks/budget-balancer.mjs
149
+ ```
150
+ Shows per-provider per-tier pressure with real token counts (e.g., `136.0K/350.0K`), weekly pressure when binding, and routing recommendation with reason.
151
+
122
152
  ## Available Tools
123
153
 
154
+ - `node .claude/hooks/wave-orchestrator.mjs "..."` — auto-wave orchestrator (plan, dispatch, test, review)
124
155
  - `node .claude/hooks/vibe-router.mjs "..."` — decompose casual requests into structured work
125
156
  - `node .claude/hooks/plan-generator.mjs --utterance "..."` — generate execution plans
126
157
  - `node .claude/hooks/vibe-memory.mjs` — persistent preferences and work threads
127
158
  - `node .claude/hooks/cost-report.mjs` — activity and cost estimates
128
159
  - `node .claude/hooks/health-check.mjs` — verify system health
129
- - `node .claude/hooks/budget-balancer.mjs` — provider balance status
160
+ - `node .claude/hooks/budget-balancer.mjs` — provider balance (token-based, real limits)
130
161
  - `node .claude/hooks/decision-ledger.mjs` — routing outcome insights
131
162
  - `node .claude/hooks/test-orchestrator.mjs` — run self-tests (40 tests)
@@ -0,0 +1,200 @@
1
+ #!/usr/bin/env node
2
+ // dual-brain — CLI entry point. Commands: init, go, status, remember, forget
3
+
4
+ import { readFileSync } from 'node:fs';
5
+ import { join, dirname } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { execSync } from 'node:child_process';
8
+
9
+ import {
10
+ ensureProfile, loadProfile, runOnboarding,
11
+ rememberPreference, forgetPreference, getActivePreferences,
12
+ getAvailableProviders, isSoloBrain, getHeadModel,
13
+ } from '../src/profile.mjs';
14
+
15
+ import { detectTask } from '../src/detect.mjs';
16
+
17
+ import {
18
+ decideRoute, getAvailableModels, estimateBudgetPressure,
19
+ } from '../src/decide.mjs';
20
+
21
+ import { dispatch, detectRuntime, dispatchDualBrain } from '../src/dispatch.mjs';
22
+
23
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
24
+
25
+ const __dirname = dirname(fileURLToPath(import.meta.url));
26
+ const PKG_PATH = join(__dirname, '..', 'package.json');
27
+
28
+ function readVersion() {
29
+ try { return JSON.parse(readFileSync(PKG_PATH, 'utf8')).version; } catch { return '0.0.0'; }
30
+ }
31
+ function flag(args, name) { const i = args.indexOf(name); return i !== -1 ? (args[i + 1] ?? true) : null; }
32
+ function err(msg) { process.stderr.write(`Error: ${msg}\n`); process.exit(1); }
33
+
34
+ function printHelp() {
35
+ console.log(`
36
+ dual-brain <command> [options]
37
+
38
+ Commands:
39
+ init First-time setup (providers, plans, optimization)
40
+ go "task description" Detect → decide → dispatch a task
41
+ --dry-run Show routing decision without executing
42
+ --files a.mjs,b.mjs Provide file context for risk classification
43
+ status Provider health, budget pressure, available models
44
+ remember "preference" Save a project-scoped preference
45
+ forget "preference" Remove a preference by fuzzy match
46
+
47
+ Options:
48
+ --version Print version
49
+ --help Show this help
50
+ `.trim());
51
+ }
52
+
53
+ // ─── Commands ─────────────────────────────────────────────────────────────────
54
+
55
+ async function cmdInit() {
56
+ const profile = await runOnboarding({ interactive: true });
57
+ const rt = await detectRuntime();
58
+ const providers = getAvailableProviders(profile);
59
+ const providerSummary = providers.length
60
+ ? providers.map(p => `${p.name === 'claude' ? 'Claude' : 'OpenAI'} (${p.plan})`).join(', ')
61
+ : 'none';
62
+ console.log(`Profile saved. Providers: ${providerSummary}. Mode: ${profile.mode}. Runtime: ${rt.runtime}`);
63
+ }
64
+
65
+ async function cmdGo(args) {
66
+ const dryRun = args.includes('--dry-run');
67
+ const filesRaw = flag(args, '--files');
68
+ const files = filesRaw && typeof filesRaw === 'string'
69
+ ? filesRaw.split(',').map(f => f.trim()).filter(Boolean)
70
+ : [];
71
+
72
+ // prompt is the first non-flag argument (or value after --dry-run which is boolean)
73
+ const prompt = args.find(a => !a.startsWith('--') && a !== (filesRaw ?? ''));
74
+ if (!prompt) err('Usage: dual-brain go "task description" [--dry-run] [--files a,b]');
75
+
76
+ const cwd = process.cwd();
77
+ const profile = await ensureProfile(cwd);
78
+ const detection = detectTask({ prompt, files });
79
+
80
+ // Print the one-sentence classification
81
+ console.log(detection.explanation);
82
+
83
+ const decision = decideRoute({ profile, detection, cwd });
84
+
85
+ // Print routing table
86
+ console.log(` provider : ${decision.provider}`);
87
+ console.log(` model : ${decision.model}${decision.effort ? ' (' + decision.effort + ')' : ''}`);
88
+ console.log(` tier : ${decision.tier}`);
89
+ console.log(` dual-brain : ${decision.dualBrain ? 'yes' : 'no'}`);
90
+ console.log(` reason : ${decision.explanation}`);
91
+
92
+ if (dryRun) {
93
+ console.log('\n(dry-run — not executing)');
94
+ return;
95
+ }
96
+
97
+ console.log('\nDispatching...');
98
+ let result;
99
+ if (decision.dualBrain) {
100
+ result = await dispatchDualBrain({ decision, prompt, files, cwd });
101
+ console.log(`\nConsensus: ${result.consensus}`);
102
+ if (result.claude?.summary) console.log(`Claude : ${result.claude.summary}`);
103
+ if (result.openai?.summary) console.log(`OpenAI : ${result.openai.summary}`);
104
+ } else {
105
+ result = await dispatch({ decision, prompt, files, cwd });
106
+ const statusLine = result.status === 'completed' ? 'Done' : `Failed (exit ${result.exitCode})`;
107
+ console.log(`\n${statusLine} in ${(result.durationMs / 1000).toFixed(1)}s`);
108
+ if (result.summary) console.log(result.summary);
109
+ if (result.error) process.stderr.write(`${result.error}\n`);
110
+ if (result.status !== 'completed') process.exit(1); }
111
+ }
112
+
113
+ async function cmdStatus() {
114
+ const cwd = process.cwd();
115
+ const profile = loadProfile(cwd);
116
+ const rt = await detectRuntime();
117
+ const providers = getAvailableProviders(profile);
118
+ const pressure = estimateBudgetPressure(profile, cwd);
119
+ const available = getAvailableModels(profile);
120
+ const prefs = getActivePreferences(cwd);
121
+
122
+ console.log('=== Dual-Brain Status ===\n');
123
+
124
+ // Providers
125
+ console.log('Providers:');
126
+ if (providers.length === 0) {
127
+ console.log(' (none configured — run: dual-brain init)');
128
+ } else {
129
+ for (const p of providers) {
130
+ const label = p.name === 'claude' ? 'Claude' : 'OpenAI';
131
+ const pct = Math.round((pressure[p.name] ?? 0) * 100);
132
+ console.log(` ${label} plan=${p.plan} budget=${pct}% used`);
133
+ }
134
+ }
135
+
136
+ // Models
137
+ console.log('\nAvailable models:');
138
+ if (available.claude.length) console.log(` Claude : ${available.claude.join(', ')}`);
139
+ if (available.openai.length) console.log(` OpenAI : ${available.openai.join(', ')}`);
140
+
141
+ // Head model
142
+ console.log(`\nHead model : ${getHeadModel(profile)}`);
143
+ console.log(`Mode : ${profile.mode}`);
144
+ console.log(`Solo brain : ${isSoloBrain(profile) ? 'yes' : 'no'}`);
145
+
146
+ // Runtime
147
+ console.log('\nRuntime:');
148
+ console.log(` claude CLI : ${rt.claudeAvailable ? 'available' : 'not found'}`);
149
+ console.log(` codex CLI : ${rt.codexAvailable ? 'available' : 'not found'}`);
150
+ console.log(` detected : ${rt.runtime}`);
151
+
152
+ // Preferences
153
+ console.log(`\nPreferences: ${prefs.length ? '' : '(none)'}`);
154
+ for (const p of prefs) console.log(` [${p.scope}] ${p.text}`);
155
+
156
+ // Update check
157
+ try {
158
+ const localVer = readVersion();
159
+ const remoteVer = execSync('npm view dual-brain version 2>/dev/null', { timeout: 5000 }).toString().trim();
160
+ if (remoteVer && remoteVer !== localVer) {
161
+ console.log(`\nUpdate available: npm i -g dual-brain@latest (${localVer} → ${remoteVer})`);
162
+ }
163
+ } catch { /* network unavailable — skip */ }
164
+ }
165
+
166
+ function cmdRemember(text) {
167
+ if (!text) err('Usage: dual-brain remember "preference text"');
168
+ const profile = rememberPreference(text, { scope: 'project', cwd: process.cwd() });
169
+ console.log(`Preference saved. Total active: ${profile.preferences.filter(p => p.enabled).length}`);
170
+ }
171
+
172
+ function cmdForget(text) {
173
+ if (!text) err('Usage: dual-brain forget "preference text"');
174
+ forgetPreference(text, process.cwd());
175
+ console.log('Preference removed (if matched).');
176
+ }
177
+
178
+ // ─── Entry point ─────────────────────────────────────────────────────────────
179
+
180
+ async function main() {
181
+ const args = process.argv.slice(2);
182
+ const cmd = args[0];
183
+
184
+ if (!cmd || cmd === '--help' || cmd === '-h') { printHelp(); return; }
185
+ if (cmd === '--version' || cmd === '-v') { console.log(readVersion()); return; }
186
+
187
+ if (cmd === 'init') { await cmdInit(); return; }
188
+ if (cmd === 'go') { await cmdGo(args.slice(1)); return; }
189
+ if (cmd === 'status') { await cmdStatus(); return; }
190
+ if (cmd === 'remember') { cmdRemember(args[1]); return; }
191
+ if (cmd === 'forget') { cmdForget(args[1]); return; }
192
+
193
+ process.stderr.write(`Unknown command: ${cmd}\nRun "dual-brain --help" for usage.\n`);
194
+ process.exit(1);
195
+ }
196
+
197
+ main().catch(e => {
198
+ process.stderr.write(`${e.message}\n`);
199
+ process.exit(1);
200
+ });