dual-brain 2.0.0 → 3.0.1

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.
Files changed (3) hide show
  1. package/README.md +58 -43
  2. package/install.mjs +419 -128
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,79 +1,94 @@
1
1
  # Dual-Brain Orchestrator
2
2
 
3
- Dual-provider orchestration for Claude Code across Claude and OpenAI subscriptions. Routes search work to cheap models, execution to mid-tier, and reserves the most capable models for thinking. Dispatches isolated tasks to GPT via Codex CLI, with dual-brain analysis for high-risk decisions.
3
+ One command. Both brains. Auto-detected. Auto-configured.
4
+
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.
4
6
 
5
7
  ## Install
6
8
 
7
9
  ```bash
8
- npx dual-brain init
10
+ npx -y dual-brain
9
11
  ```
10
12
 
11
- Then run the setup wizard:
13
+ That's it. The installer auto-detects your environment:
14
+ - Finds Claude CLI and checks auth status
15
+ - Finds Codex CLI and checks auth status
16
+ - Detects Replit and replit-tools if present
17
+ - Configures dual-provider, Claude-only, or OpenAI-only mode automatically
18
+ - Registers hooks in `.claude/settings.json`
19
+ - No wizard. No restart. No manual steps.
20
+
21
+ Run it again anytime — it's idempotent. Re-detects providers, updates hooks, preserves your config.
22
+
23
+ ### Unlock full features
12
24
 
13
25
  ```bash
14
- node .claude/hooks/setup-wizard.mjs
15
- ```
26
+ # Claude (you probably have this already)
27
+ claude login
16
28
 
17
- Restart your Claude Code session. The wizard configures `orchestrator.json` with the right models and cost rates for your subscription tier.
29
+ # OpenAI (optional enables GPT lane + dual-brain)
30
+ npm i -g @openai/codex
31
+ codex login
18
32
 
19
- **What the installer does:**
20
- - Copies 13 hook scripts to `.claude/hooks/`
21
- - Copies orchestrator config and hookify rules to `.claude/`
22
- - Registers `enforce-tier.mjs` (PreToolUse) and `cost-logger.mjs` (PostToolUse) in `.claude/settings.json`
23
- - Creates a `review-rules.md` template for your project-specific GPT review rules
24
- - Updates `.gitignore` to exclude usage logs and review artifacts
33
+ # Re-run to detect new providers
34
+ npx -y dual-brain
35
+ ```
25
36
 
26
37
  ## How it works
27
38
 
28
- Two hooks are registered in `.claude/settings.json` and run automatically:
39
+ **Two hooks fire automatically** (registered in `.claude/settings.json`):
40
+
41
+ - **enforce-tier.mjs** (PreToolUse on Agent): Classifies tasks, advises the correct model, detects duplicates, suggests cross-provider routing
42
+ - **cost-logger.mjs** (PostToolUse on all tools): Logs usage to daily rotated files for cost tracking
29
43
 
30
- - **enforce-tier.mjs** (PreToolUse on Agent): Classifies agent tasks by keyword, advises the correct model tier, detects duplicates, and suggests cross-provider routing
31
- - **cost-logger.mjs** (PostToolUse on all tools): Logs usage data to daily rotated files for cost tracking
44
+ **Three tiers route work by complexity:**
32
45
 
33
- Three hookify rules in `.claude/hookify.orchestrator-*.local.md` provide session-level guidance:
46
+ | Tier | Claude | OpenAI | Use for |
47
+ |------|--------|--------|---------|
48
+ | Search | Haiku | GPT-4.1-mini | grep, explore, file reads |
49
+ | Execute | Sonnet | GPT-5.4 | edits, tests, git ops |
50
+ | Think | Opus | GPT-5.5 | architecture, review, planning |
34
51
 
35
- - **Route**: Reminds the session to delegate subagents at the right tier
36
- - **Gate**: Catches code changes that weren't reviewed before the session ends
37
- - **Cost**: Checks that dispatched subagents use the correct model tier
52
+ **Dual-brain** kicks in automatically for high-risk decisions both providers think on the same problem independently.
38
53
 
39
54
  ## Scripts
40
55
 
41
56
  | Script | Purpose |
42
57
  |--------|---------|
43
- | `hooks/setup-wizard.mjs` | Interactive setup — configure your subscription and preferences |
44
58
  | `hooks/cost-report.mjs` | Activity & cost estimates by model tier |
45
- | `hooks/dual-brain-review.mjs` | Send current git diff to GPT for independent review |
59
+ | `hooks/dual-brain-review.mjs` | Send git diff to GPT for independent review |
46
60
  | `hooks/dual-brain-think.mjs` | Dual-perspective analysis on architecture decisions |
47
- | `hooks/quality-gate.mjs` | Config-driven quality gate with sensitivity scoring |
48
- | `hooks/budget-balancer.mjs` | Show provider balance and routing recommendations |
61
+ | `hooks/quality-gate.mjs` | Sensitivity-scored quality gate with review artifacts |
62
+ | `hooks/budget-balancer.mjs` | Provider balance and routing recommendations |
49
63
  | `hooks/gpt-work-dispatcher.mjs` | Dispatch execution tasks to GPT via Codex CLI |
50
- | `hooks/session-report.mjs` | Session-end summary: activity, routing compliance, quality gate |
51
- | `hooks/health-check.mjs` | Verify all hooks and dependencies are configured |
52
- | `hooks/test-orchestrator.mjs` | Self-test harness validates all hooks work correctly |
53
- | `hooks/install-git-hooks.mjs` | Install a git pre-commit hook for the quality gate |
54
- | `hooks/enforce-tier.mjs` | PreToolUse hook enforces model tier routing (automatic) |
55
- | `hooks/cost-logger.mjs` | PostToolUse hook — logs usage data (automatic) |
64
+ | `hooks/session-report.mjs` | Session-end summary: activity, compliance, quality |
65
+ | `hooks/health-check.mjs` | Verify all hooks and dependencies are working |
66
+ | `hooks/test-orchestrator.mjs` | Self-test harness (14 tests) |
67
+ | `hooks/setup-wizard.mjs` | Interactive config (optional for custom plans) |
68
+ | `hooks/install-git-hooks.mjs` | Git pre-commit hook for quality gate |
56
69
 
57
- ## Model Intelligence
70
+ ## CLI options
58
71
 
59
- The `model_intelligence` section in `orchestrator.json` provides per-model metadata: strengths, weaknesses, best-for/avoid-for task guidance, context windows, and Codex compatibility. The `enforce-tier.mjs` hook reads this to give context-aware routing advice.
72
+ ```bash
73
+ npx -y dual-brain # detect, configure, install
74
+ npx dual-brain --force # overwrite all config
75
+ npx dual-brain --dry-run # detect only, don't write
76
+ npx dual-brain --json # output detection as JSON
77
+ npx dual-brain --help # show help
78
+ ```
60
79
 
61
80
  ## Customize
62
81
 
63
- - `orchestrator.json` — subscriptions, tiers, quality gate, routing rules, budgets
64
- - `.claude/review-rules.md` — project-specific rules injected into GPT review prompts
65
- - `.claude/settings.json` — hook registrations (auto-generated by installer)
82
+ After install, edit these files:
83
+
84
+ - `orchestrator.json` — subscriptions, tiers, quality gate, budgets, routing
85
+ - `review-rules.md` — project-specific rules for GPT code review
86
+ - `settings.json` — hook registrations (auto-generated, safe to extend)
66
87
 
67
88
  ## Requirements
68
89
 
69
90
  - Node 20+
70
- - Codex CLI (optional) for GPT-lane features: `npm i -g @openai/codex` then `codex login`. Falls back to `OPENAI_API_KEY` env var. Without Codex, Claude-lane features work normally.
71
-
72
- ## Works with any subscription
73
-
74
- The setup wizard supports any combination:
75
- - Claude only ($20 Pro / $100 Max / $200 Max / API)
76
- - OpenAI only ($20 Plus / $100 Pro)
77
- - Both providers (recommended for dual-brain features)
91
+ - Claude Code (any subscription tier)
92
+ - Codex CLI (optional) — `npm i -g @openai/codex && codex login`
78
93
 
79
- Without an OpenAI subscription, GPT-lane features gracefully degrade — all work routes through Claude.
94
+ Works with any subscription combination. Without OpenAI, GPT features gracefully degrade — all work routes through Claude.
package/install.mjs CHANGED
@@ -1,153 +1,444 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * dual-brain — Install the Dual-Brain Orchestrator into your project.
3
+ * dual-brain — Dual-provider orchestrator for Claude Code.
4
4
  *
5
5
  * Usage:
6
- * npx dual-brain init [--force]
6
+ * npx -y dual-brain # auto-detect, configure, done
7
+ * npx dual-brain --force # overwrite existing config
8
+ * npx dual-brain --dry-run # detect only, don't install
7
9
  * npx dual-brain --help
8
10
  */
9
11
  import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
10
12
  import { dirname, join, resolve } from 'path';
11
13
  import { fileURLToPath } from 'url';
14
+ import { spawnSync } from 'child_process';
12
15
 
13
16
  const __dirname = dirname(fileURLToPath(import.meta.url));
14
- const args = process.argv.slice(2);
15
- const command = args.find(a => !a.startsWith('-'));
16
- const force = args.includes('--force');
17
-
18
- const W = 50;
19
- const border = (l, r) => l + '═'.repeat(W) + r;
20
- const line = (text) => {
21
- const padded = String(text).padEnd(W - 2);
22
- return `║ ${padded.slice(0, W - 2)} ║`;
23
- };
17
+ const VERSION = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf8')).version;
24
18
 
25
- if (args.includes('--help') || args.includes('-h') || (!command && !force)) {
26
- console.log('');
27
- console.log(' Usage: npx dual-brain init [--force]');
28
- console.log('');
29
- console.log(' Commands:');
30
- console.log(' init Install orchestrator into .claude/');
31
- console.log('');
32
- console.log(' Options:');
33
- console.log(' --force Overwrite existing .claude/ hooks');
34
- console.log(' --help Show this help message');
35
- console.log('');
19
+ // ─── CLI ────────────────────────────────────────────────────────────────────
20
+
21
+ const argv = process.argv.slice(2);
22
+ const flag = (f) => argv.includes(f);
23
+ const force = flag('--force');
24
+ const dryRun = flag('--dry-run');
25
+ const jsonOut = flag('--json');
26
+
27
+ if (flag('--help') || flag('-h')) {
28
+ console.log(`
29
+ dual-brain v${VERSION} — Dual-provider orchestrator for Claude Code
30
+
31
+ Usage: npx -y dual-brain [options]
32
+
33
+ Options:
34
+ --force Overwrite all existing config (keeps review-rules.md)
35
+ --dry-run Detect environment only, don't install
36
+ --json Output detection as JSON (implies --dry-run)
37
+ --help Show this help
38
+ `);
36
39
  process.exit(0);
37
40
  }
38
41
 
39
- if (command && command !== 'init') {
40
- console.error(` Unknown command: ${command}`);
42
+ // Silently accept 'init' for backward compat
43
+ const positional = argv.filter(a => !a.startsWith('-'));
44
+ if (positional.length > 0 && positional[0] !== 'init') {
45
+ console.error(` Unknown command: ${positional[0]}`);
41
46
  console.error(' Run: npx dual-brain --help');
42
47
  process.exit(1);
43
48
  }
44
49
 
45
- const TARGET = resolve(process.cwd(), '.claude');
50
+ // ─── Box Drawing ────────────────────────────────────────────────────────────
46
51
 
47
- console.log('');
48
- console.log(` ${border('╔', '╗')}`);
49
- console.log(` ${line('Dual-Brain Orchestrator Installer')}`);
50
- console.log(` ${border('╚', '')}`);
51
- console.log('');
52
+ const W = 54;
53
+ const pad = (s, len = W - 2) => {
54
+ s = String(s);
55
+ return s.length >= len ? s.slice(0, len) : s + ' '.repeat(len - s.length);
56
+ };
57
+ const ln = (s) => `║ ${pad(s)} ║`;
58
+ const br = (l, r) => l + '═'.repeat(W) + r;
59
+ const sep = () => '╠' + '═'.repeat(W) + '╣';
52
60
 
53
- if (existsSync(TARGET) && !force) {
54
- console.log(' .claude/ directory already exists.');
55
- console.log(' Use --force to overwrite, or run the setup wizard:');
56
- console.log(' node .claude/hooks/setup-wizard.mjs');
57
- console.log('');
58
- process.exit(1);
61
+ // ─── Detection ──────────────────────────────────────────────────────────────
62
+
63
+ function run(cmd, args, opts = {}) {
64
+ return spawnSync(cmd, args, {
65
+ encoding: 'utf8',
66
+ stdio: ['pipe', 'pipe', 'pipe'],
67
+ timeout: 8000,
68
+ ...opts,
69
+ });
70
+ }
71
+
72
+ function detectReplit() {
73
+ const isReplit = !!(process.env.REPL_ID || process.env.REPL_SLUG);
74
+ const hasReplitTools = existsSync(resolve(process.cwd(), '.replit-tools'));
75
+ return { isReplit, hasReplitTools };
76
+ }
77
+
78
+ function detectClaude() {
79
+ const result = { installed: false, version: null, authed: false };
80
+
81
+ const ver = run('claude', ['--version']);
82
+ if (ver.status === 0 && ver.stdout.trim()) {
83
+ result.installed = true;
84
+ result.version = ver.stdout.trim().split('\n')[0];
85
+ }
86
+
87
+ if (!result.installed) {
88
+ const which = run('which', ['claude']);
89
+ if (which.status === 0 && which.stdout.trim()) result.installed = true;
90
+ }
91
+
92
+ const credPaths = [
93
+ join(process.env.HOME || '', '.claude', '.credentials.json'),
94
+ join(process.env.HOME || '', '.claude', 'credentials.json'),
95
+ resolve(process.cwd(), '.replit-tools', '.claude-persistent', '.credentials.json'),
96
+ ];
97
+ for (const p of credPaths) {
98
+ try {
99
+ const cred = JSON.parse(readFileSync(p, 'utf8'));
100
+ if (cred.claudeAiOauth || cred.apiKey || cred.oauth_token) {
101
+ result.authed = true;
102
+ break;
103
+ }
104
+ } catch {}
105
+ }
106
+
107
+ if (!result.authed && result.installed) {
108
+ const auth = run('claude', ['auth', 'status']);
109
+ const out = ((auth.stdout || '') + (auth.stderr || '')).toLowerCase();
110
+ if (out.includes('logged in') || out.includes('authenticated') || out.includes('valid')) {
111
+ result.authed = true;
112
+ }
113
+ }
114
+
115
+ return result;
59
116
  }
60
117
 
61
- mkdirSync(join(TARGET, 'hooks'), { recursive: true });
62
-
63
- const HOOKS = [
64
- 'enforce-tier.mjs', 'cost-logger.mjs', 'cost-report.mjs',
65
- 'dual-brain-review.mjs', 'dual-brain-think.mjs', 'quality-gate.mjs',
66
- 'test-orchestrator.mjs', 'setup-wizard.mjs', 'health-check.mjs',
67
- 'install-git-hooks.mjs', 'session-report.mjs', 'budget-balancer.mjs',
68
- 'gpt-work-dispatcher.mjs',
69
- ];
70
-
71
- for (const hook of HOOKS) {
72
- cpSync(join(__dirname, 'hooks', hook), join(TARGET, 'hooks', hook));
73
- }
74
- console.log(` ✓ Copied ${HOOKS.length} hook scripts`);
75
-
76
- const CONFIGS = [
77
- 'orchestrator.json',
78
- 'CLAUDE.md',
79
- 'hookify.orchestrator-route.local.md',
80
- 'hookify.orchestrator-gate.local.md',
81
- 'hookify.orchestrator-cost.local.md',
82
- ];
83
-
84
- for (const cfg of CONFIGS) {
85
- cpSync(join(__dirname, cfg), join(TARGET, cfg));
86
- }
87
- console.log(' Copied orchestrator config');
88
-
89
- const rulesTarget = join(TARGET, 'review-rules.md');
90
- if (!existsSync(rulesTarget)) {
91
- cpSync(join(__dirname, 'review-rules.md'), rulesTarget);
92
- console.log(' ✓ Created review-rules.md template');
93
- } else {
94
- console.log(' ⊘ review-rules.md already exists, skipping');
95
- }
96
-
97
- const settingsPath = join(TARGET, 'settings.json');
98
- let settings = {};
99
- try { settings = JSON.parse(readFileSync(settingsPath, 'utf8')); } catch {}
100
-
101
- const hooksConfig = {
102
- PreToolUse: [
103
- {
104
- matcher: 'Agent',
105
- hooks: [{ type: 'command', command: `node ${join('.claude', 'hooks', 'enforce-tier.mjs')}` }],
106
- },
107
- ],
108
- PostToolUse: [
109
- {
110
- matcher: '',
111
- hooks: [{ type: 'command', command: `node ${join('.claude', 'hooks', 'cost-logger.mjs')}` }],
112
- },
113
- ],
118
+ function detectCodex() {
119
+ const result = { installed: false, version: null, authed: false, path: null };
120
+
121
+ const which = run('which', ['codex']);
122
+ if (which.status === 0 && which.stdout.trim()) {
123
+ result.path = which.stdout.trim();
124
+ result.installed = true;
125
+ }
126
+
127
+ if (!result.installed) {
128
+ const home = process.env.HOME || '';
129
+ const fallbacks = [
130
+ join(home, '.local', 'bin', 'codex'),
131
+ join(home, 'bin', 'codex'),
132
+ '/usr/local/bin/codex',
133
+ ];
134
+ for (const p of fallbacks) {
135
+ if (existsSync(p)) { result.path = p; result.installed = true; break; }
136
+ }
137
+ }
138
+
139
+ if (result.installed && result.path) {
140
+ const ver = run(result.path, ['--version']);
141
+ if (ver.status === 0) result.version = ver.stdout.trim().split('\n')[0];
142
+
143
+ const login = run(result.path, ['login', 'status']);
144
+ const out = ((login.stdout || '') + (login.stderr || '')).toLowerCase();
145
+ if (login.status === 0 || out.includes('logged in') || out.includes('authenticated')) {
146
+ result.authed = true;
147
+ }
148
+ }
149
+
150
+ return result;
151
+ }
152
+
153
+ function detectExisting(workspace) {
154
+ const claude = resolve(workspace, '.claude');
155
+ return {
156
+ hasClaudeDir: existsSync(claude),
157
+ hasOrchestrator: existsSync(join(claude, 'orchestrator.json')),
158
+ hasSettings: existsSync(join(claude, 'settings.json')),
159
+ hasHooks: existsSync(join(claude, 'hooks', 'enforce-tier.mjs')),
160
+ };
161
+ }
162
+
163
+ function detectEnvironment() {
164
+ return {
165
+ ...detectReplit(),
166
+ claude: detectClaude(),
167
+ codex: detectCodex(),
168
+ existing: detectExisting(process.cwd()),
169
+ workspace: resolve(process.cwd()),
170
+ };
171
+ }
172
+
173
+ // ─── Mode Resolution ────────────────────────────────────────────────────────
174
+
175
+ function resolveMode(env) {
176
+ const c = env.claude.authed || env.claude.installed;
177
+ const o = env.codex.authed;
178
+ if (c && o) return { mode: 'dual', claudeEnabled: true, openaiEnabled: true };
179
+ if (c) return { mode: 'claude-only', claudeEnabled: true, openaiEnabled: false };
180
+ if (o) return { mode: 'openai-only', claudeEnabled: false, openaiEnabled: true };
181
+ return { mode: 'detect-only', claudeEnabled: true, openaiEnabled: false };
182
+ }
183
+
184
+ const MODE_LABELS = {
185
+ 'dual': 'dual-provider (full features)',
186
+ 'claude-only': 'Claude only (GPT features available when Codex authed)',
187
+ 'openai-only': 'OpenAI + Claude (auth Claude for full features)',
188
+ 'detect-only': 'hooks installed (auth providers to activate)',
114
189
  };
115
190
 
116
- settings.hooks = { ...(settings.hooks || {}), ...hooksConfig };
117
- writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
118
- console.log(' ✓ Registered hooks in .claude/settings.json');
119
-
120
- const gitignorePath = resolve(process.cwd(), '.gitignore');
121
- const ignoreEntries = [
122
- '.claude/hooks/usage-*.jsonl',
123
- '.claude/hooks/usage.jsonl',
124
- '.claude/reviews/',
125
- '.claude/hooks/.drift-warned',
126
- '.claude/hooks/.budget-alerted',
127
- ];
128
-
129
- let gitignore = '';
130
- try { gitignore = readFileSync(gitignorePath, 'utf8'); } catch {}
131
-
132
- const newEntries = ignoreEntries.filter(e => !gitignore.includes(e));
133
- if (newEntries.length > 0) {
134
- const block = '\n# Dual-Brain Orchestrator\n' + newEntries.join('\n') + '\n';
135
- writeFileSync(gitignorePath, gitignore + block);
136
- console.log(' ✓ Updated .gitignore');
137
- }
138
-
139
- console.log('');
140
- console.log(` ${border('╔', '╗')}`);
141
- console.log(` ${line('Installed!')}`);
142
- console.log(` ${border('╠', '╣')}`);
143
- console.log(` ${line('Next steps:')}`);
144
- console.log(` ${line('1. node .claude/hooks/setup-wizard.mjs')}`);
145
- console.log(` ${line('2. Restart your Claude Code session')}`);
146
- console.log(` ${line('3. node .claude/hooks/health-check.mjs')}`);
147
- console.log(` ${border('╠', '╣')}`);
148
- console.log(` ${line('Optional:')}`);
149
- console.log(` ${line('• Edit .claude/review-rules.md for your repo')}`);
150
- console.log(` ${line('• node .claude/hooks/install-git-hooks.mjs')}`);
151
- console.log(` ${line('• node .claude/hooks/test-orchestrator.mjs')}`);
152
- console.log(` ${border('╚', '╝')}`);
153
- console.log('');
191
+ // ─── Config Generation ──────────────────────────────────────────────────────
192
+
193
+ function generateOrchestrator(mode, workspace) {
194
+ const template = JSON.parse(readFileSync(join(__dirname, 'orchestrator.json'), 'utf8'));
195
+ const existing = {};
196
+ const existingPath = join(workspace, '.claude', 'orchestrator.json');
197
+ try { Object.assign(existing, JSON.parse(readFileSync(existingPath, 'utf8'))); } catch {}
198
+
199
+ const config = force ? { ...template } : { ...template, ...existing };
200
+
201
+ config.providers = config.providers || template.providers;
202
+ config.providers.claude = { ...(template.providers?.claude || {}), ...(config.providers?.claude || {}) };
203
+ config.providers.openai = { ...(template.providers?.openai || {}), ...(config.providers?.openai || {}) };
204
+ config.providers.claude.enabled = mode.claudeEnabled;
205
+ config.providers.openai.enabled = mode.openaiEnabled;
206
+
207
+ config.dual_thinking = config.dual_thinking || template.dual_thinking;
208
+ config.dual_thinking.enabled = mode.mode === 'dual';
209
+
210
+ config.subscriptions = config.subscriptions || template.subscriptions;
211
+ config.model_intelligence = config.model_intelligence || template.model_intelligence;
212
+ config.tiers = config.tiers || template.tiers;
213
+ config.quality_gate = force ? template.quality_gate : (config.quality_gate || template.quality_gate);
214
+ config.routing_rules = force ? template.routing_rules : (config.routing_rules || template.routing_rules);
215
+ config.budgets = force ? template.budgets : (config.budgets || template.budgets);
216
+ config.routing = force ? template.routing : (config.routing || template.routing);
217
+ config.codex_skills = template.codex_skills;
218
+ config.pricing_verified = new Date().toISOString().slice(0, 10);
219
+
220
+ return config;
221
+ }
222
+
223
+ function generateSettings(workspace) {
224
+ const settingsPath = join(workspace, '.claude', 'settings.json');
225
+ let existing = {};
226
+ try { existing = JSON.parse(readFileSync(settingsPath, 'utf8')); } catch {}
227
+
228
+ const hooks = {
229
+ PreToolUse: [
230
+ {
231
+ matcher: 'Agent',
232
+ hooks: [{ type: 'command', command: 'node .claude/hooks/enforce-tier.mjs' }],
233
+ },
234
+ ],
235
+ PostToolUse: [
236
+ {
237
+ matcher: '',
238
+ hooks: [{ type: 'command', command: 'node .claude/hooks/cost-logger.mjs' }],
239
+ },
240
+ ],
241
+ };
242
+
243
+ return { ...existing, hooks };
244
+ }
245
+
246
+ function generateClaudeMd(mode) {
247
+ let md = readFileSync(join(__dirname, 'CLAUDE.md'), 'utf8');
248
+
249
+ if (mode.mode === 'claude-only') {
250
+ md = md.replace(
251
+ /## GPT Lane[\s\S]*?(?=## )/,
252
+ '## GPT Lane\n\nGPT features activate automatically when Codex CLI is authenticated (`npm i -g @openai/codex && codex login`).\n\n'
253
+ );
254
+ } else if (mode.mode === 'detect-only') {
255
+ md = '# Dual-Brain Orchestrator\n\nHooks installed but no providers authenticated yet.\nRun `npx dual-brain` again after authenticating Claude or Codex.\n\n' + md.split('\n').slice(3).join('\n');
256
+ }
257
+
258
+ return md;
259
+ }
260
+
261
+ function generateGitignoreEntries(workspace) {
262
+ const entries = [
263
+ '.claude/hooks/usage-*.jsonl',
264
+ '.claude/hooks/usage.jsonl',
265
+ '.claude/reviews/',
266
+ '.claude/hooks/.drift-warned',
267
+ '.claude/hooks/.budget-alerted',
268
+ ];
269
+ let existing = '';
270
+ try { existing = readFileSync(join(workspace, '.gitignore'), 'utf8'); } catch {}
271
+ const needed = entries.filter(e => !existing.includes(e));
272
+ return { existing, needed };
273
+ }
274
+
275
+ // ─── Installation ───────────────────────────────────────────────────────────
276
+
277
+ function install(workspace, env, mode) {
278
+ const target = join(workspace, '.claude');
279
+ const actions = [];
280
+
281
+ mkdirSync(join(target, 'hooks'), { recursive: true });
282
+
283
+ const HOOKS = [
284
+ 'enforce-tier.mjs', 'cost-logger.mjs', 'cost-report.mjs',
285
+ 'dual-brain-review.mjs', 'dual-brain-think.mjs', 'quality-gate.mjs',
286
+ 'test-orchestrator.mjs', 'setup-wizard.mjs', 'health-check.mjs',
287
+ 'install-git-hooks.mjs', 'session-report.mjs', 'budget-balancer.mjs',
288
+ 'gpt-work-dispatcher.mjs',
289
+ ];
290
+ for (const h of HOOKS) cpSync(join(__dirname, 'hooks', h), join(target, 'hooks', h));
291
+ actions.push(`✓ ${HOOKS.length} hook scripts`);
292
+
293
+ const RULES = [
294
+ 'hookify.orchestrator-route.local.md',
295
+ 'hookify.orchestrator-gate.local.md',
296
+ 'hookify.orchestrator-cost.local.md',
297
+ ];
298
+ for (const r of RULES) cpSync(join(__dirname, r), join(target, r));
299
+ actions.push(`✓ ${RULES.length} hookify rules`);
300
+
301
+ const orch = generateOrchestrator(mode, workspace);
302
+ writeFileSync(join(target, 'orchestrator.json'), JSON.stringify(orch, null, 2) + '\n');
303
+ actions.push(`✓ orchestrator.json (${mode.mode})`);
304
+
305
+ const settings = generateSettings(workspace);
306
+ writeFileSync(join(target, 'settings.json'), JSON.stringify(settings, null, 2) + '\n');
307
+ actions.push('✓ settings.json (hooks registered)');
308
+
309
+ const claudeMd = generateClaudeMd(mode);
310
+ writeFileSync(join(target, 'CLAUDE.md'), claudeMd);
311
+ actions.push('✓ CLAUDE.md (session instructions)');
312
+
313
+ const rulesTarget = join(target, 'review-rules.md');
314
+ if (!existsSync(rulesTarget) || force) {
315
+ cpSync(join(__dirname, 'review-rules.md'), rulesTarget);
316
+ actions.push('✓ review-rules.md template');
317
+ } else {
318
+ actions.push('⊘ review-rules.md (kept yours)');
319
+ }
320
+
321
+ const { existing: gi, needed } = generateGitignoreEntries(workspace);
322
+ if (needed.length > 0) {
323
+ writeFileSync(
324
+ join(workspace, '.gitignore'),
325
+ gi + '\n# Dual-Brain Orchestrator\n' + needed.join('\n') + '\n'
326
+ );
327
+ actions.push('✓ .gitignore updated');
328
+ }
329
+
330
+ return actions;
331
+ }
332
+
333
+ // ─── Status Report ──────────────────────────────────────────────────────────
334
+
335
+ function statusIcon(val) { return val ? '✓' : '✗'; }
336
+
337
+ function printReport(env, mode, actions) {
338
+ const lines = [];
339
+
340
+ lines.push(br('╔', '╗'));
341
+ lines.push(ln(`Dual-Brain Orchestrator v${VERSION}`));
342
+ lines.push(sep());
343
+
344
+ lines.push(ln('Environment'));
345
+ if (env.isReplit) {
346
+ lines.push(ln(` Platform: Replit${env.hasReplitTools ? ' (replit-tools detected)' : ''}`));
347
+ } else {
348
+ lines.push(ln(' Platform: standalone'));
349
+ }
350
+
351
+ const cVer = env.claude.version ? ` ${env.claude.version}` : '';
352
+ const cAuth = env.claude.authed ? 'authenticated' : env.claude.installed ? 'not authenticated' : 'not found';
353
+ lines.push(ln(` Claude CLI: ${statusIcon(env.claude.authed)} ${cAuth}${cVer}`));
354
+
355
+ const xVer = env.codex.version ? ` ${env.codex.version}` : '';
356
+ const xAuth = env.codex.authed ? 'authenticated' : env.codex.installed ? 'not authenticated' : 'not found';
357
+ lines.push(ln(` Codex CLI: ${statusIcon(env.codex.authed)} ${xAuth}${xVer}`));
358
+
359
+ lines.push(sep());
360
+ lines.push(ln(`Mode: ${MODE_LABELS[mode.mode]}`));
361
+
362
+ if (actions) {
363
+ lines.push(sep());
364
+ lines.push(ln('Installed'));
365
+ for (const a of actions) lines.push(ln(` ${a}`));
366
+ }
367
+
368
+ const needsAction = !env.claude.authed || !env.codex.authed;
369
+ if (needsAction && mode.mode !== 'dual') {
370
+ lines.push(sep());
371
+ lines.push(ln('To unlock full features:'));
372
+ if (!env.claude.installed) {
373
+ lines.push(ln(' curl -fsSL https://claude.ai/install.sh | sh'));
374
+ }
375
+ if (!env.claude.authed) {
376
+ lines.push(ln(' claude login'));
377
+ }
378
+ if (!env.codex.installed) {
379
+ lines.push(ln(' npm i -g @openai/codex'));
380
+ }
381
+ if (!env.codex.authed && env.codex.installed) {
382
+ lines.push(ln(' codex login'));
383
+ }
384
+ lines.push(ln(' Then run: npx dual-brain'));
385
+ }
386
+
387
+ lines.push(sep());
388
+ if (actions) {
389
+ lines.push(ln(mode.mode === 'dual'
390
+ ? 'Ready — both providers active, no restart needed'
391
+ : 'Ready — hooks active, run commands above for full power'));
392
+ } else {
393
+ lines.push(ln('Dry run — no files written'));
394
+ }
395
+ lines.push(br('╚', '╝'));
396
+
397
+ console.log('');
398
+ for (const l of lines) console.log(` ${l}`);
399
+ console.log('');
400
+
401
+ if (actions) {
402
+ console.log(' What just happened:');
403
+ console.log(' Every Claude Code session in this project now auto-routes');
404
+ console.log(' agent work by complexity — cheap models for search, mid-tier');
405
+ console.log(' for execution, best models for thinking. Cost is tracked.');
406
+ if (mode.mode === 'dual') {
407
+ console.log(' Both Claude and GPT are available as work providers.');
408
+ }
409
+ console.log('');
410
+ console.log(' Try these in your next Claude Code session:');
411
+ console.log(' node .claude/hooks/health-check.mjs # verify setup');
412
+ console.log(' node .claude/hooks/cost-report.mjs # see activity');
413
+ console.log(' node .claude/hooks/budget-balancer.mjs # provider balance');
414
+ if (mode.openaiEnabled) {
415
+ console.log(' node .claude/hooks/dual-brain-review.mjs # GPT code review');
416
+ }
417
+ console.log('');
418
+ console.log(' Customize:');
419
+ console.log(' .claude/review-rules.md # your project\'s review rules');
420
+ console.log(' .claude/orchestrator.json # routing, budgets, tiers');
421
+ console.log('');
422
+ }
423
+ }
424
+
425
+ // ─── Main ───────────────────────────────────────────────────────────────────
426
+
427
+ function main() {
428
+ const env = detectEnvironment();
429
+ const mode = resolveMode(env);
430
+
431
+ if (dryRun || jsonOut) {
432
+ if (jsonOut) {
433
+ console.log(JSON.stringify({ version: VERSION, env, mode }, null, 2));
434
+ } else {
435
+ printReport(env, mode, null);
436
+ }
437
+ process.exit(0);
438
+ }
439
+
440
+ const actions = install(env.workspace, env, mode);
441
+ printReport(env, mode, actions);
442
+ }
443
+
444
+ main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dual-brain",
3
- "version": "2.0.0",
3
+ "version": "3.0.1",
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": {