dual-brain 2.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 +40 -0
- package/LICENSE +21 -0
- package/README.md +79 -0
- package/hookify.orchestrator-cost.local.md +16 -0
- package/hookify.orchestrator-gate.local.md +19 -0
- package/hookify.orchestrator-route.local.md +23 -0
- package/hooks/budget-balancer.mjs +463 -0
- package/hooks/cost-logger.mjs +250 -0
- package/hooks/cost-report.mjs +344 -0
- package/hooks/dual-brain-review.mjs +302 -0
- package/hooks/dual-brain-think.mjs +321 -0
- package/hooks/enforce-tier.mjs +282 -0
- package/hooks/gpt-work-dispatcher.mjs +254 -0
- package/hooks/health-check.mjs +390 -0
- package/hooks/install-git-hooks.mjs +106 -0
- package/hooks/quality-gate.mjs +283 -0
- package/hooks/session-report.mjs +514 -0
- package/hooks/setup-wizard.mjs +130 -0
- package/hooks/test-orchestrator.mjs +316 -0
- package/install.mjs +153 -0
- package/orchestrator.json +215 -0
- package/package.json +38 -0
- package/review-rules.md +17 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* test-orchestrator.mjs — Self-test harness for all dual-brain orchestrator hooks.
|
|
4
|
+
*
|
|
5
|
+
* Usage: node .claude/hooks/test-orchestrator.mjs
|
|
6
|
+
*
|
|
7
|
+
* Runs a suite of fast tests against the hook scripts, prints PASS/FAIL per
|
|
8
|
+
* test, and exits with code 0 if all pass, 1 if any fail.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { execSync, spawnSync } from 'child_process';
|
|
12
|
+
import {
|
|
13
|
+
existsSync,
|
|
14
|
+
readFileSync,
|
|
15
|
+
writeFileSync,
|
|
16
|
+
} from 'fs';
|
|
17
|
+
import { dirname, resolve } from 'path';
|
|
18
|
+
import { fileURLToPath } from 'url';
|
|
19
|
+
|
|
20
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const HOOKS = __dirname;
|
|
22
|
+
|
|
23
|
+
const ENFORCE_TIER = resolve(HOOKS, 'enforce-tier.mjs');
|
|
24
|
+
const COST_LOGGER = resolve(HOOKS, 'cost-logger.mjs');
|
|
25
|
+
const DUAL_BRAIN = resolve(HOOKS, 'dual-brain-review.mjs');
|
|
26
|
+
const ORCHESTRATOR = resolve(HOOKS, '..', 'orchestrator.json');
|
|
27
|
+
const USAGE_JSONL = resolve(HOOKS, `usage-${new Date().toISOString().slice(0, 10)}.jsonl`);
|
|
28
|
+
|
|
29
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Run a hook script, passing stdinData through a shell pipe so that
|
|
33
|
+
* readFileSync('/dev/stdin') inside the script can read it correctly.
|
|
34
|
+
*
|
|
35
|
+
* We use `sh -c "echo '<json>' | node <script>"` so that /dev/stdin is a
|
|
36
|
+
* real pipe file descriptor, not a spawnSync input buffer.
|
|
37
|
+
*/
|
|
38
|
+
function run(scriptPath, stdinData, extraEnv = {}) {
|
|
39
|
+
// Escape single quotes in the JSON payload for use inside single-quoted shell string
|
|
40
|
+
const escaped = (stdinData || '').replace(/'/g, "'\\''");
|
|
41
|
+
const shellCmd = `printf '%s' '${escaped}' | ${process.execPath} ${scriptPath}`;
|
|
42
|
+
|
|
43
|
+
const proc = spawnSync('sh', ['-c', shellCmd], {
|
|
44
|
+
encoding: 'utf8',
|
|
45
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
46
|
+
env: { ...process.env, ...extraEnv },
|
|
47
|
+
timeout: 8_000,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
let parsed = null;
|
|
51
|
+
try { parsed = JSON.parse((proc.stdout || '').trim()); } catch {}
|
|
52
|
+
return { raw: proc.stdout || '', stderr: proc.stderr || '', parsed, status: proc.status };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Run a hook that reads from a for-await stdin loop (cost-logger style),
|
|
57
|
+
* using spawnSync with the input option (works for stream-based reads).
|
|
58
|
+
*/
|
|
59
|
+
function runStream(scriptPath, stdinData, extraEnv = {}) {
|
|
60
|
+
const proc = spawnSync(process.execPath, [scriptPath], {
|
|
61
|
+
input: stdinData || '',
|
|
62
|
+
encoding: 'utf8',
|
|
63
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
64
|
+
env: { ...process.env, ...extraEnv },
|
|
65
|
+
timeout: 8_000,
|
|
66
|
+
});
|
|
67
|
+
let parsed = null;
|
|
68
|
+
try { parsed = JSON.parse((proc.stdout || '').trim()); } catch {}
|
|
69
|
+
return { raw: proc.stdout || '', stderr: proc.stderr || '', parsed, status: proc.status };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let passed = 0;
|
|
73
|
+
let failed = 0;
|
|
74
|
+
|
|
75
|
+
function test(name, fn) {
|
|
76
|
+
try {
|
|
77
|
+
const result = fn();
|
|
78
|
+
if (result === true) {
|
|
79
|
+
console.log(`PASS ${name}`);
|
|
80
|
+
passed++;
|
|
81
|
+
} else {
|
|
82
|
+
console.log(`FAIL ${name}${result ? ` — ${result}` : ''}`);
|
|
83
|
+
failed++;
|
|
84
|
+
}
|
|
85
|
+
} catch (err) {
|
|
86
|
+
console.log(`FAIL ${name} — threw: ${err?.message ?? String(err)}`);
|
|
87
|
+
failed++;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ─── Test 1: enforce-tier: search with opus ───────────────────────────────────
|
|
92
|
+
test('enforce-tier: search with opus', () => {
|
|
93
|
+
const payload = JSON.stringify({
|
|
94
|
+
tool_name: 'Agent',
|
|
95
|
+
tool_input: { prompt: 'find auth files', model: 'opus', subagent_type: 'Explore' },
|
|
96
|
+
});
|
|
97
|
+
const { parsed } = run(ENFORCE_TIER, payload);
|
|
98
|
+
if (!parsed) return 'no valid JSON output';
|
|
99
|
+
if (!parsed.systemMessage) return `expected systemMessage, got: ${JSON.stringify(parsed)}`;
|
|
100
|
+
if (!parsed.systemMessage.toLowerCase().includes('haiku'))
|
|
101
|
+
return `expected "haiku" in systemMessage, got: ${parsed.systemMessage}`;
|
|
102
|
+
return true;
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// ─── Test 2: enforce-tier: correct tier ──────────────────────────────────────
|
|
106
|
+
test('enforce-tier: correct tier', () => {
|
|
107
|
+
const payload = JSON.stringify({
|
|
108
|
+
tool_name: 'Agent',
|
|
109
|
+
tool_input: { prompt: `unique test prompt ${Date.now()}`, model: 'sonnet' },
|
|
110
|
+
});
|
|
111
|
+
const { parsed } = run(ENFORCE_TIER, payload);
|
|
112
|
+
if (!parsed) return 'no valid JSON output';
|
|
113
|
+
// Should return {} or at most a drift warning (not a tier mismatch)
|
|
114
|
+
if (parsed.systemMessage && parsed.systemMessage.includes('Tier Enforcer'))
|
|
115
|
+
return `unexpected tier mismatch: ${parsed.systemMessage}`;
|
|
116
|
+
return true;
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// ─── Test 3: enforce-tier: think task on haiku ───────────────────────────────
|
|
120
|
+
test('enforce-tier: think on haiku', () => {
|
|
121
|
+
const payload = JSON.stringify({
|
|
122
|
+
tool_name: 'Agent',
|
|
123
|
+
tool_input: { prompt: 'review security', model: 'haiku' },
|
|
124
|
+
});
|
|
125
|
+
const { parsed } = run(ENFORCE_TIER, payload);
|
|
126
|
+
if (!parsed) return 'no valid JSON output';
|
|
127
|
+
if (!parsed.systemMessage)
|
|
128
|
+
return `expected systemMessage warning, got: ${JSON.stringify(parsed)}`;
|
|
129
|
+
return true;
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// ─── Test 4: enforce-tier: non-Agent tool ────────────────────────────────────
|
|
133
|
+
test('enforce-tier: non-Agent tool', () => {
|
|
134
|
+
const payload = JSON.stringify({
|
|
135
|
+
tool_name: 'Bash',
|
|
136
|
+
tool_input: { command: 'ls' },
|
|
137
|
+
});
|
|
138
|
+
const { parsed } = run(ENFORCE_TIER, payload);
|
|
139
|
+
if (!parsed) return 'no valid JSON output';
|
|
140
|
+
if (Object.keys(parsed).length !== 0)
|
|
141
|
+
return `expected {}, got: ${JSON.stringify(parsed)}`;
|
|
142
|
+
return true;
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// ─── Test 5: enforce-tier: missing config (bad JSON in config path) ───────────
|
|
146
|
+
test('enforce-tier: missing config', () => {
|
|
147
|
+
// enforce-tier catches config read errors and falls back to {} — verify that
|
|
148
|
+
// an Agent payload still exits cleanly when config can't be parsed.
|
|
149
|
+
// We set HOME to /tmp/nonexistent-orch-test so readFileSync of the hardcoded
|
|
150
|
+
// config path will fail (the path is hardcoded, but we can't easily redirect
|
|
151
|
+
// it). Instead, verify that sending a model string that matches no known tier
|
|
152
|
+
// still results in a clean non-crashing exit.
|
|
153
|
+
const payload = JSON.stringify({
|
|
154
|
+
tool_name: 'Agent',
|
|
155
|
+
tool_input: { prompt: 'do something', model: 'unknown-model-xyz' },
|
|
156
|
+
});
|
|
157
|
+
const { parsed, status } = run(ENFORCE_TIER, payload);
|
|
158
|
+
// Should exit 0 and produce valid JSON (either {} or a systemMessage)
|
|
159
|
+
if (status !== 0) return `non-zero exit: ${status}`;
|
|
160
|
+
if (!parsed) return 'no valid JSON output';
|
|
161
|
+
return true;
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// ─── Test 6: cost-logger: logs entry ─────────────────────────────────────────
|
|
165
|
+
test('cost-logger: logs entry', () => {
|
|
166
|
+
// Record current line count of usage.jsonl before the test.
|
|
167
|
+
let linesBefore = 0;
|
|
168
|
+
if (existsSync(USAGE_JSONL)) {
|
|
169
|
+
linesBefore = readFileSync(USAGE_JSONL, 'utf8').split('\n').filter(Boolean).length;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const payload = JSON.stringify({
|
|
173
|
+
tool_name: 'Read',
|
|
174
|
+
tool_input: { file_path: '/some/file.ts' },
|
|
175
|
+
});
|
|
176
|
+
// cost-logger uses for-await on process.stdin → use runStream (spawnSync input pipe)
|
|
177
|
+
const { parsed, status } = runStream(COST_LOGGER, payload);
|
|
178
|
+
|
|
179
|
+
if (status !== 0) return `non-zero exit: ${status}`;
|
|
180
|
+
if (!parsed || Object.keys(parsed).length !== 0)
|
|
181
|
+
return `expected {}, got: ${JSON.stringify(parsed)}`;
|
|
182
|
+
|
|
183
|
+
if (!existsSync(USAGE_JSONL)) return 'daily usage log was not created';
|
|
184
|
+
|
|
185
|
+
const lines = readFileSync(USAGE_JSONL, 'utf8').split('\n').filter(Boolean);
|
|
186
|
+
const linesAfter = lines.length;
|
|
187
|
+
if (linesAfter <= linesBefore) return 'no new line was appended to daily usage log';
|
|
188
|
+
|
|
189
|
+
// Validate the new entry is valid JSON with expected fields
|
|
190
|
+
const lastLine = lines[linesAfter - 1];
|
|
191
|
+
let entry;
|
|
192
|
+
try { entry = JSON.parse(lastLine); } catch { return `last line not valid JSON: ${lastLine}`; }
|
|
193
|
+
if (!entry.timestamp) return 'entry missing timestamp';
|
|
194
|
+
if (!entry.tier) return 'entry missing tier';
|
|
195
|
+
if (!entry.tool) return 'entry missing tool';
|
|
196
|
+
|
|
197
|
+
// Clean up the test line we just added
|
|
198
|
+
try {
|
|
199
|
+
const kept = lines.slice(0, linesBefore).join('\n');
|
|
200
|
+
writeFileSync(USAGE_JSONL, kept ? kept + '\n' : '', 'utf8');
|
|
201
|
+
} catch {
|
|
202
|
+
// Best-effort cleanup; don't fail the test over it
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return true;
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// ─── Test 7: dual-brain: valid output ────────────────────────────────────────
|
|
209
|
+
test('dual-brain: valid output', () => {
|
|
210
|
+
// Run dual-brain-review.mjs in a temp git repo with no changes so the test
|
|
211
|
+
// is deterministic and never triggers codex/API calls on a dirty working tree.
|
|
212
|
+
const tmpDir = spawnSync('mktemp', ['-d'], { encoding: 'utf8' }).stdout.trim();
|
|
213
|
+
try {
|
|
214
|
+
execSync(
|
|
215
|
+
`git init -q "${tmpDir}" && git -C "${tmpDir}" commit --allow-empty -m init -q`,
|
|
216
|
+
{ stdio: 'pipe' }
|
|
217
|
+
);
|
|
218
|
+
const proc = spawnSync(process.execPath, [DUAL_BRAIN], {
|
|
219
|
+
cwd: tmpDir,
|
|
220
|
+
encoding: 'utf8',
|
|
221
|
+
timeout: 10_000,
|
|
222
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
223
|
+
});
|
|
224
|
+
// status null means the process was killed (timeout/signal) — treat as fail
|
|
225
|
+
if (proc.status == null) return `process killed or timed out (signal/null status)`;
|
|
226
|
+
if (proc.status !== 0) return `non-zero exit: ${proc.status}`;
|
|
227
|
+
let parsed = null;
|
|
228
|
+
try { parsed = JSON.parse((proc.stdout || '').trim()); } catch {}
|
|
229
|
+
if (!parsed) return `no valid JSON output; raw: ${(proc.stdout || '').slice(0, 200)}`;
|
|
230
|
+
if (typeof parsed.review !== 'string') return `expected review string, got: ${JSON.stringify(parsed)}`;
|
|
231
|
+
return true;
|
|
232
|
+
} finally {
|
|
233
|
+
spawnSync('rm', ['-rf', tmpDir], { stdio: 'pipe' });
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// ─── Test 8: orchestrator.json: valid JSON ────────────────────────────────────
|
|
238
|
+
test('orchestrator.json: valid JSON', () => {
|
|
239
|
+
if (!existsSync(ORCHESTRATOR)) return 'orchestrator.json not found';
|
|
240
|
+
let config;
|
|
241
|
+
try {
|
|
242
|
+
config = JSON.parse(readFileSync(ORCHESTRATOR, 'utf8'));
|
|
243
|
+
} catch (err) {
|
|
244
|
+
return `invalid JSON: ${err.message}`;
|
|
245
|
+
}
|
|
246
|
+
if (!config.quality_gate) return 'missing quality_gate section';
|
|
247
|
+
if (!config.tiers) return 'missing tiers section';
|
|
248
|
+
if (!config.subscriptions) return 'missing subscriptions section';
|
|
249
|
+
return true;
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// ─── Test 9: enforce-tier: think on gpt-4.1-mini ─────────────────────────────
|
|
253
|
+
test('enforce-tier: think on gpt-4.1-mini', () => {
|
|
254
|
+
const input = JSON.stringify({ tool_name: 'Agent', tool_input: { description: 'review security architecture', prompt: 'audit auth', model: 'gpt-4.1-mini' } });
|
|
255
|
+
const { parsed } = run(ENFORCE_TIER, input);
|
|
256
|
+
if (!parsed) return 'no valid JSON output';
|
|
257
|
+
if (!parsed.systemMessage) return `expected systemMessage warning, got: ${JSON.stringify(parsed)}`;
|
|
258
|
+
if (!parsed.systemMessage.toLowerCase().includes('think'))
|
|
259
|
+
return `expected "think" in systemMessage, got: ${parsed.systemMessage}`;
|
|
260
|
+
return true;
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// ─── Test 10: orchestrator.json: model_intelligence ──────────────────────────
|
|
264
|
+
test('orchestrator.json: model_intelligence', () => {
|
|
265
|
+
const config = JSON.parse(readFileSync(resolve(__dirname, '..', 'orchestrator.json'), 'utf8'));
|
|
266
|
+
const mi = config.model_intelligence;
|
|
267
|
+
if (!mi) return 'model_intelligence key missing';
|
|
268
|
+
if (!mi.opus) return 'model_intelligence missing opus entry';
|
|
269
|
+
if (!mi.sonnet) return 'model_intelligence missing sonnet entry';
|
|
270
|
+
if (!mi.haiku) return 'model_intelligence missing haiku entry';
|
|
271
|
+
return true;
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// ─── Test 11: orchestrator.json: pricing_verified ────────────────────────────
|
|
275
|
+
test('orchestrator.json: pricing_verified', () => {
|
|
276
|
+
const config = JSON.parse(readFileSync(resolve(__dirname, '..', 'orchestrator.json'), 'utf8'));
|
|
277
|
+
if (!config.pricing_verified) return 'pricing_verified field missing';
|
|
278
|
+
if (isNaN(Date.parse(config.pricing_verified))) return `pricing_verified is not a valid date: ${config.pricing_verified}`;
|
|
279
|
+
return true;
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// ─── Test 12: budget-balancer: loads and runs ────────────────────────────────
|
|
283
|
+
test('budget-balancer: loads and runs', () => {
|
|
284
|
+
const proc = spawnSync(process.execPath, [resolve(__dirname, 'budget-balancer.mjs')], {
|
|
285
|
+
encoding: 'utf8',
|
|
286
|
+
timeout: 10000,
|
|
287
|
+
cwd: resolve(__dirname, '..', '..'),
|
|
288
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
289
|
+
});
|
|
290
|
+
if (proc.status !== 0) return `exit code ${proc.status}: ${proc.stderr}`;
|
|
291
|
+
if (!proc.stdout.includes('Provider Balance')) return 'missing output header';
|
|
292
|
+
return true;
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// ─── Test 13: orchestrator.json: providers configured ────────────────────────
|
|
296
|
+
test('orchestrator.json: providers configured', () => {
|
|
297
|
+
const config = JSON.parse(readFileSync(resolve(__dirname, '..', 'orchestrator.json'), 'utf8'));
|
|
298
|
+
if (!config.providers?.claude?.enabled) return 'claude provider not enabled';
|
|
299
|
+
if (!config.providers?.openai?.enabled) return 'openai provider not enabled';
|
|
300
|
+
if (!config.routing?.strategy) return 'routing strategy missing';
|
|
301
|
+
return true;
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// ─── Test 14: orchestrator.json: dual_thinking configured ────────────────────
|
|
305
|
+
test('orchestrator.json: dual_thinking configured', () => {
|
|
306
|
+
const config = JSON.parse(readFileSync(resolve(__dirname, '..', 'orchestrator.json'), 'utf8'));
|
|
307
|
+
if (!config.dual_thinking?.enabled) return 'dual_thinking not enabled';
|
|
308
|
+
if (!config.dual_thinking?.auto_triggers?.length) return 'no auto_triggers';
|
|
309
|
+
if (!config.dual_thinking?.sensitive_paths?.length) return 'no sensitive_paths';
|
|
310
|
+
return true;
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// ─── Summary ─────────────────────────────────────────────────────────────────
|
|
314
|
+
const total = passed + failed;
|
|
315
|
+
console.log(`\n${passed}/${total} tests passed`);
|
|
316
|
+
process.exit(failed > 0 ? 1 : 0);
|
package/install.mjs
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* dual-brain — Install the Dual-Brain Orchestrator into your project.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* npx dual-brain init [--force]
|
|
7
|
+
* npx dual-brain --help
|
|
8
|
+
*/
|
|
9
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
10
|
+
import { dirname, join, resolve } from 'path';
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
12
|
+
|
|
13
|
+
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
|
+
};
|
|
24
|
+
|
|
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('');
|
|
36
|
+
process.exit(0);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (command && command !== 'init') {
|
|
40
|
+
console.error(` Unknown command: ${command}`);
|
|
41
|
+
console.error(' Run: npx dual-brain --help');
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const TARGET = resolve(process.cwd(), '.claude');
|
|
46
|
+
|
|
47
|
+
console.log('');
|
|
48
|
+
console.log(` ${border('╔', '╗')}`);
|
|
49
|
+
console.log(` ${line('Dual-Brain Orchestrator Installer')}`);
|
|
50
|
+
console.log(` ${border('╚', '╝')}`);
|
|
51
|
+
console.log('');
|
|
52
|
+
|
|
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);
|
|
59
|
+
}
|
|
60
|
+
|
|
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
|
+
],
|
|
114
|
+
};
|
|
115
|
+
|
|
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('');
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
{
|
|
2
|
+
"subscriptions": {
|
|
3
|
+
"claude": {
|
|
4
|
+
"plan": "$100",
|
|
5
|
+
"models": {
|
|
6
|
+
"opus": { "tier": "think", "input_per_mtok": 5.0, "output_per_mtok": 25.0, "context_window": 1000000, "max_output": 128000 },
|
|
7
|
+
"sonnet": { "tier": "execute", "input_per_mtok": 3.0, "output_per_mtok": 15.0, "context_window": 1000000, "max_output": 64000 },
|
|
8
|
+
"haiku": { "tier": "search", "input_per_mtok": 1.0, "output_per_mtok": 5.0, "context_window": 200000, "max_output": 64000 }
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"openai": {
|
|
12
|
+
"plan": "$100",
|
|
13
|
+
"models": {
|
|
14
|
+
"gpt-5.5": { "tier": "think", "input_per_mtok": 5.0, "output_per_mtok": 30.0, "context_window": 1000000, "max_output": 128000 },
|
|
15
|
+
"gpt-5.4": { "tier": "execute", "input_per_mtok": 2.5, "output_per_mtok": 15.0, "context_window": 1000000, "max_output": 128000 },
|
|
16
|
+
"gpt-4.1-mini": { "tier": "search", "input_per_mtok": 0.40, "output_per_mtok": 1.60, "context_window": 1047576, "max_output": 32768 }
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
"model_intelligence": {
|
|
22
|
+
"opus": {
|
|
23
|
+
"model_id": "claude-opus-4-6",
|
|
24
|
+
"strengths": ["agentic coding", "complex reasoning", "tool use", "error recovery", "architecture decisions"],
|
|
25
|
+
"weaknesses": ["higher latency", "tokenizer tax on 4.7 (12-35% more tokens)"],
|
|
26
|
+
"best_for": "architecture, security review, complex debugging, multi-step planning",
|
|
27
|
+
"avoid_for": "simple file reads, grep, formatting — wasteful at this tier"
|
|
28
|
+
},
|
|
29
|
+
"sonnet": {
|
|
30
|
+
"model_id": "claude-sonnet-4-6",
|
|
31
|
+
"strengths": ["best speed/intelligence ratio", "precise minimal diffs", "1M context", "extended thinking"],
|
|
32
|
+
"weaknesses": ["less reliable on complex multi-step reasoning than Opus"],
|
|
33
|
+
"best_for": "implementation, refactoring, test writing, code edits, git operations",
|
|
34
|
+
"avoid_for": "architecture decisions, security audits — upgrade to think tier"
|
|
35
|
+
},
|
|
36
|
+
"haiku": {
|
|
37
|
+
"model_id": "claude-haiku-4-5-20251001",
|
|
38
|
+
"strengths": ["fastest latency", "cheapest", "good enough for read-only tasks"],
|
|
39
|
+
"weaknesses": ["200k context (vs 1M for others)", "weaker reasoning", "older knowledge cutoff"],
|
|
40
|
+
"best_for": "file lookups, grep, explore, read-only research, listing files",
|
|
41
|
+
"avoid_for": "any task requiring edits, reasoning, or judgment"
|
|
42
|
+
},
|
|
43
|
+
"gpt-5.5": {
|
|
44
|
+
"model_id": "gpt-5.5",
|
|
45
|
+
"strengths": ["complex reasoning", "fast interactive responses", "strong code review", "independent perspective from Claude"],
|
|
46
|
+
"weaknesses": ["different failure modes than Claude — feature not bug for dual-brain"],
|
|
47
|
+
"best_for": "independent code review (dual-brain), second opinions on architecture",
|
|
48
|
+
"codex_compatible": true
|
|
49
|
+
},
|
|
50
|
+
"gpt-5.4": {
|
|
51
|
+
"model_id": "gpt-5.4",
|
|
52
|
+
"strengths": ["good speed/cost ratio", "1M context", "strong instruction following"],
|
|
53
|
+
"weaknesses": ["less capable reasoning than gpt-5.5"],
|
|
54
|
+
"best_for": "implementation and execution tasks via Codex",
|
|
55
|
+
"codex_compatible": true
|
|
56
|
+
},
|
|
57
|
+
"gpt-4.1-mini": {
|
|
58
|
+
"model_id": "gpt-4.1-mini",
|
|
59
|
+
"strengths": ["extremely cheap", "good instruction following", "1M context"],
|
|
60
|
+
"weaknesses": ["32k max output", "no reasoning chain"],
|
|
61
|
+
"best_for": "cheap search/lookup tasks via Codex",
|
|
62
|
+
"codex_compatible": true
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
"tiers": {
|
|
67
|
+
"search": {
|
|
68
|
+
"description": "Read-only lookups, exploration, grep, find, file reads",
|
|
69
|
+
"prefer": "cheapest available model",
|
|
70
|
+
"tasks": ["explore", "grep", "find", "ls", "read_file", "git_log", "git_status"]
|
|
71
|
+
},
|
|
72
|
+
"execute": {
|
|
73
|
+
"description": "Implementation, edits, test runs, git operations, linting",
|
|
74
|
+
"prefer": "mid-tier model",
|
|
75
|
+
"tasks": ["edit", "write", "test_run", "lint", "format", "simple_fix", "refactor_small"]
|
|
76
|
+
},
|
|
77
|
+
"think": {
|
|
78
|
+
"description": "Architecture, review, planning, security, complex debugging",
|
|
79
|
+
"prefer": "most capable model",
|
|
80
|
+
"tasks": ["architecture", "review", "planning", "security", "complex_debug", "design"]
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
"quality_gate": {
|
|
85
|
+
"enabled": true,
|
|
86
|
+
"trigger_extensions": [".ts", ".tsx", ".js", ".jsx", ".py", ".rs", ".go", ".java", ".rb", ".swift", ".kt"],
|
|
87
|
+
"skip_patterns": ["test", "__tests__", "spec", ".md", ".json", ".yaml", ".toml", ".txt"]
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
"routing_rules": {
|
|
91
|
+
"subagent_defaults": {
|
|
92
|
+
"Explore": "search",
|
|
93
|
+
"general-purpose": "execute",
|
|
94
|
+
"Plan": "think",
|
|
95
|
+
"code-reviewer": "think"
|
|
96
|
+
},
|
|
97
|
+
"max_concurrent_think": 1,
|
|
98
|
+
"max_concurrent_execute": 3,
|
|
99
|
+
"max_concurrent_search": 4,
|
|
100
|
+
"model_routing_note": "Claude Code model: param may be silently ignored (issue #43869). Set CLAUDE_CODE_SUBAGENT_MODEL env var as fallback if subagents all run on parent model."
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
"codex_skills": {
|
|
104
|
+
"review": {
|
|
105
|
+
"description": "Independent GPT code review via ChatGPT subscription",
|
|
106
|
+
"command": "node .claude/hooks/dual-brain-review.mjs",
|
|
107
|
+
"requires": "codex_auth"
|
|
108
|
+
},
|
|
109
|
+
"quality_gate": {
|
|
110
|
+
"description": "Config-driven quality gate with review artifacts",
|
|
111
|
+
"command": "node .claude/hooks/quality-gate.mjs"
|
|
112
|
+
},
|
|
113
|
+
"cost_report": {
|
|
114
|
+
"description": "Session activity and cost estimate by model tier",
|
|
115
|
+
"command": "node .claude/hooks/cost-report.mjs"
|
|
116
|
+
},
|
|
117
|
+
"test": {
|
|
118
|
+
"description": "Self-test harness for all orchestrator hooks",
|
|
119
|
+
"command": "node .claude/hooks/test-orchestrator.mjs"
|
|
120
|
+
},
|
|
121
|
+
"health_check": {
|
|
122
|
+
"description": "Verify all hooks are wired and system is healthy",
|
|
123
|
+
"command": "node .claude/hooks/health-check.mjs"
|
|
124
|
+
},
|
|
125
|
+
"session_report": {
|
|
126
|
+
"description": "Comprehensive session-end summary: activity by tier, routing compliance, quality gate status, data quality, drift warnings",
|
|
127
|
+
"command": "node .claude/hooks/session-report.mjs"
|
|
128
|
+
},
|
|
129
|
+
"setup_wizard": {
|
|
130
|
+
"description": "Interactive setup wizard — configures Claude Code hooks, dual-provider routing (Claude + OpenAI/Codex), subscription tiers, and cost-tracking settings",
|
|
131
|
+
"command": "node .claude/hooks/setup-wizard.mjs"
|
|
132
|
+
},
|
|
133
|
+
"gpt_dispatch": {
|
|
134
|
+
"description": "Dispatch execution tasks to GPT via Codex CLI",
|
|
135
|
+
"command": "node .claude/hooks/gpt-work-dispatcher.mjs"
|
|
136
|
+
},
|
|
137
|
+
"budget_balance": {
|
|
138
|
+
"description": "Show provider balance status and routing recommendations",
|
|
139
|
+
"command": "node .claude/hooks/budget-balancer.mjs"
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
"pricing_verified": "2026-05-13",
|
|
144
|
+
|
|
145
|
+
"budgets": {
|
|
146
|
+
"session_warn_usd": 5.00,
|
|
147
|
+
"session_limit_usd": 10.00,
|
|
148
|
+
"daily_warn_usd": 20.00,
|
|
149
|
+
"daily_limit_usd": 50.00,
|
|
150
|
+
"alert_cooldown_minutes": 15
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
"routing": {
|
|
154
|
+
"strategy": "hybrid-specialized-balanced",
|
|
155
|
+
"codex_startup_penalty_ms": 20000,
|
|
156
|
+
"min_codex_task_ms": 180000,
|
|
157
|
+
"dual_thinking_permission_threshold": {
|
|
158
|
+
"estimated_tokens": 50000,
|
|
159
|
+
"agent_count": 2
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
"providers": {
|
|
164
|
+
"claude": {
|
|
165
|
+
"enabled": true,
|
|
166
|
+
"subscription": "max-5x",
|
|
167
|
+
"models": {
|
|
168
|
+
"think": "opus",
|
|
169
|
+
"execute": "sonnet",
|
|
170
|
+
"search": "haiku"
|
|
171
|
+
},
|
|
172
|
+
"rolling_window_hours": 5,
|
|
173
|
+
"pressure_thresholds": {
|
|
174
|
+
"warm": 0.65,
|
|
175
|
+
"hot": 0.82,
|
|
176
|
+
"throttled": 0.95
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
"openai": {
|
|
180
|
+
"enabled": true,
|
|
181
|
+
"subscription": "pro",
|
|
182
|
+
"codex_command": "codex",
|
|
183
|
+
"models": {
|
|
184
|
+
"think": "gpt-5.5",
|
|
185
|
+
"execute": "gpt-5.4",
|
|
186
|
+
"search": "gpt-4.1-mini"
|
|
187
|
+
},
|
|
188
|
+
"rolling_window_hours": 5,
|
|
189
|
+
"pressure_thresholds": {
|
|
190
|
+
"warm": 0.65,
|
|
191
|
+
"hot": 0.82,
|
|
192
|
+
"throttled": 0.95
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
"dual_thinking": {
|
|
198
|
+
"enabled": true,
|
|
199
|
+
"auto_triggers": [
|
|
200
|
+
"architecture",
|
|
201
|
+
"security",
|
|
202
|
+
"database-migration",
|
|
203
|
+
"public-api",
|
|
204
|
+
"large-refactor",
|
|
205
|
+
"dependency-upgrade",
|
|
206
|
+
"production-incident"
|
|
207
|
+
],
|
|
208
|
+
"sensitive_paths": [
|
|
209
|
+
"auth", "security", "middleware/auth", "payment", "billing",
|
|
210
|
+
"migration", "schema", "permissions", "secrets", "crypto",
|
|
211
|
+
"api/public", ".env"
|
|
212
|
+
],
|
|
213
|
+
"conflict_resolver": "least-pressured-think-model"
|
|
214
|
+
}
|
|
215
|
+
}
|