dual-brain 3.2.0 → 3.3.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 +20 -1
- package/hooks/control-panel.mjs +489 -0
- package/hooks/dual-brain-review.mjs +106 -17
- package/hooks/dual-brain-think.mjs +81 -17
- package/install.mjs +90 -79
- package/package.json +1 -1
package/CLAUDE.md
CHANGED
|
@@ -15,7 +15,26 @@ Route subagents by task complexity:
|
|
|
15
15
|
For isolated or parallel work, dispatch to GPT via Codex CLI:
|
|
16
16
|
|
|
17
17
|
- `node .claude/hooks/gpt-work-dispatcher.mjs --task "..." --model gpt-5.4` — execution tasks
|
|
18
|
-
|
|
18
|
+
|
|
19
|
+
## Dual-Brain Collaboration
|
|
20
|
+
|
|
21
|
+
Dual-brain is a multi-round conversation between Claude and GPT — not a single-shot dispatch.
|
|
22
|
+
|
|
23
|
+
**Think flow** (architecture decisions):
|
|
24
|
+
1. Round 1: `node .claude/hooks/dual-brain-think.mjs --question "..."`
|
|
25
|
+
→ GPT gives independent analysis
|
|
26
|
+
2. You analyze the same question independently
|
|
27
|
+
3. Round 2: `node .claude/hooks/dual-brain-think.mjs --question "..." --round 2 --claude-says "<your analysis>"`
|
|
28
|
+
→ GPT responds to your points: agreements, pushback, refined recommendation
|
|
29
|
+
4. You synthesize both rounds into a final decision
|
|
30
|
+
|
|
31
|
+
**Review flow** (code review):
|
|
32
|
+
1. Round 1: `node .claude/hooks/dual-brain-review.mjs`
|
|
33
|
+
→ GPT reviews the diff independently
|
|
34
|
+
2. You review the same diff independently
|
|
35
|
+
3. Round 2: `node .claude/hooks/dual-brain-review.mjs --round 2 --claude-review "<your findings>"`
|
|
36
|
+
→ GPT confirms shared findings, acknowledges misses, disputes false positives
|
|
37
|
+
4. You synthesize into a final review verdict
|
|
19
38
|
|
|
20
39
|
## Routing Rules
|
|
21
40
|
|
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* control-panel.mjs — Interactive TUI control panel for Dual-Brain Orchestrator.
|
|
4
|
+
*
|
|
5
|
+
* Keyboard-driven dashboard with live-updating pressure, profile switching,
|
|
6
|
+
* inline budget editing, and routing decision viewer.
|
|
7
|
+
*
|
|
8
|
+
* Falls back to static emoji output when not in a TTY.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import readline from 'readline';
|
|
12
|
+
import { existsSync, readFileSync, renameSync, writeFileSync } from 'fs';
|
|
13
|
+
import { dirname, join } from 'path';
|
|
14
|
+
import { fileURLToPath } from 'url';
|
|
15
|
+
import { spawnSync } from 'child_process';
|
|
16
|
+
|
|
17
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const CONFIG_FILE = join(__dirname, '..', 'orchestrator.json');
|
|
19
|
+
const PROFILE_FILE = join(__dirname, '..', 'dual-brain.profile.json');
|
|
20
|
+
const VERSION = (() => {
|
|
21
|
+
try { return JSON.parse(readFileSync(join(__dirname, '..', '..', 'dual-brain', 'package.json'), 'utf8')).version; } catch {}
|
|
22
|
+
try { return JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8')).version; } catch {}
|
|
23
|
+
return '?';
|
|
24
|
+
})();
|
|
25
|
+
|
|
26
|
+
// ─── ANSI ──────────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
const color = !process.env.NO_COLOR;
|
|
29
|
+
const A = {
|
|
30
|
+
altOn: '\x1b[?1049h', altOff: '\x1b[?1049l',
|
|
31
|
+
clear: '\x1b[2J', home: '\x1b[H',
|
|
32
|
+
hide: '\x1b[?25l', show: '\x1b[?25h',
|
|
33
|
+
reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
|
|
34
|
+
red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m',
|
|
35
|
+
blue: '\x1b[34m', cyan: '\x1b[36m', gray: '\x1b[90m',
|
|
36
|
+
white: '\x1b[37m',
|
|
37
|
+
};
|
|
38
|
+
const c = (code, s) => color ? `${code}${s}${A.reset}` : s;
|
|
39
|
+
|
|
40
|
+
// ─── Profiles ──────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
const PROFILES = {
|
|
43
|
+
balanced: { emoji: '⚖️', label: 'Balanced', desc: 'Standard routing — best model per tier' },
|
|
44
|
+
'cost-saver': { emoji: '💸', label: 'Cost-saver', desc: 'Minimize spend — prefer cheaper models' },
|
|
45
|
+
'quality-first': { emoji: '💎', label: 'Quality-first', desc: 'Maximum quality — dual-brain for medium+' },
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const PROFILE_BUDGETS = {
|
|
49
|
+
balanced: { session_warn_usd: 5, session_limit_usd: 10, daily_warn_usd: 20, daily_limit_usd: 50 },
|
|
50
|
+
'cost-saver': { session_warn_usd: 2, session_limit_usd: 5, daily_warn_usd: 8, daily_limit_usd: 20 },
|
|
51
|
+
'quality-first': { session_warn_usd: 15, session_limit_usd: 30, daily_warn_usd: 50, daily_limit_usd: 100 },
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const PROFILE_GATE = {
|
|
55
|
+
balanced: { sensitivity_floor: 'medium', dual_brain_minimum: 'high' },
|
|
56
|
+
'cost-saver': { sensitivity_floor: 'high', dual_brain_minimum: 'critical' },
|
|
57
|
+
'quality-first': { sensitivity_floor: 'low', dual_brain_minimum: 'medium' },
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// ─── Data Loaders ──────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
function loadConfig() {
|
|
63
|
+
try { return JSON.parse(readFileSync(CONFIG_FILE, 'utf8')); } catch { return {}; }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function loadProfile() {
|
|
67
|
+
try {
|
|
68
|
+
const data = JSON.parse(readFileSync(PROFILE_FILE, 'utf8'));
|
|
69
|
+
const name = data.active && PROFILES[data.active] ? data.active : 'balanced';
|
|
70
|
+
const custom = data.custom_overrides || {};
|
|
71
|
+
return {
|
|
72
|
+
name,
|
|
73
|
+
budgets: { ...PROFILE_BUDGETS[name], ...custom.budgets },
|
|
74
|
+
gate: PROFILE_GATE[name],
|
|
75
|
+
switched_at: data.switched_at || null,
|
|
76
|
+
};
|
|
77
|
+
} catch {
|
|
78
|
+
return { name: 'balanced', budgets: PROFILE_BUDGETS.balanced, gate: PROFILE_GATE.balanced, switched_at: null };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function saveProfile(name, customOverrides) {
|
|
83
|
+
const data = { active: name, switched_at: new Date().toISOString() };
|
|
84
|
+
if (customOverrides) data.custom_overrides = customOverrides;
|
|
85
|
+
const tmp = PROFILE_FILE + '.tmp.' + process.pid;
|
|
86
|
+
writeFileSync(tmp, JSON.stringify(data, null, 2) + '\n');
|
|
87
|
+
renameSync(tmp, PROFILE_FILE);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function saveBudget(sessionLimit, dailyLimit) {
|
|
91
|
+
let existing = {};
|
|
92
|
+
try { existing = JSON.parse(readFileSync(PROFILE_FILE, 'utf8')); } catch {}
|
|
93
|
+
const custom = existing.custom_overrides || {};
|
|
94
|
+
custom.budgets = {
|
|
95
|
+
session_warn_usd: +(sessionLimit * 0.6).toFixed(2),
|
|
96
|
+
session_limit_usd: sessionLimit,
|
|
97
|
+
daily_warn_usd: +(dailyLimit * 0.6).toFixed(2),
|
|
98
|
+
daily_limit_usd: dailyLimit,
|
|
99
|
+
};
|
|
100
|
+
const data = { active: existing.active || 'balanced', switched_at: existing.switched_at || new Date().toISOString(), custom_overrides: custom };
|
|
101
|
+
const tmp = PROFILE_FILE + '.tmp.' + process.pid;
|
|
102
|
+
writeFileSync(tmp, JSON.stringify(data, null, 2) + '\n');
|
|
103
|
+
renameSync(tmp, PROFILE_FILE);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function detectProviders() {
|
|
107
|
+
const claude = { authed: false, models: 'opus / sonnet / haiku' };
|
|
108
|
+
const codex = { authed: false, installed: false, models: 'gpt-5.5 / gpt-5.4 / gpt-4.1-mini' };
|
|
109
|
+
|
|
110
|
+
const credPaths = [
|
|
111
|
+
join(process.env.HOME || '', '.claude', '.credentials.json'),
|
|
112
|
+
join(process.env.HOME || '', '.claude', 'credentials.json'),
|
|
113
|
+
];
|
|
114
|
+
for (const p of credPaths) {
|
|
115
|
+
try {
|
|
116
|
+
const cred = JSON.parse(readFileSync(p, 'utf8'));
|
|
117
|
+
if (cred.claudeAiOauth || cred.apiKey || cred.oauth_token) { claude.authed = true; break; }
|
|
118
|
+
} catch {}
|
|
119
|
+
}
|
|
120
|
+
if (!claude.authed) {
|
|
121
|
+
const r = spawnSync('claude', ['auth', 'status'], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 5000 });
|
|
122
|
+
const out = ((r.stdout || '') + (r.stderr || '')).toLowerCase();
|
|
123
|
+
if (out.includes('logged in') || out.includes('authenticated')) claude.authed = true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const which = spawnSync('which', ['codex'], { encoding: 'utf8', stdio: 'pipe', timeout: 3000 });
|
|
127
|
+
if (which.status === 0 && which.stdout.trim()) {
|
|
128
|
+
codex.installed = true;
|
|
129
|
+
const login = spawnSync(which.stdout.trim(), ['login', 'status'], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 5000 });
|
|
130
|
+
const out = ((login.stdout || '') + (login.stderr || '')).toLowerCase();
|
|
131
|
+
if (login.status === 0 || out.includes('logged in') || out.includes('authenticated')) codex.authed = true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return { claude, codex };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function loadPressure() {
|
|
138
|
+
try {
|
|
139
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
140
|
+
const summaryPath = join(__dirname, `usage-summary-${today}.json`);
|
|
141
|
+
const summary = JSON.parse(readFileSync(summaryPath, 'utf8'));
|
|
142
|
+
const cutoff = Date.now() - 5 * 60 * 60 * 1000;
|
|
143
|
+
const result = {};
|
|
144
|
+
for (const provider of ['claude', 'openai']) {
|
|
145
|
+
result[provider] = {};
|
|
146
|
+
for (const tier of ['think', 'execute', 'search']) {
|
|
147
|
+
const ts = (summary.pressure?.[provider]?.[tier] || []).filter(t => Date.parse(t) >= cutoff);
|
|
148
|
+
const BUDGETS = { think: 45, execute: 364, search: 2000 };
|
|
149
|
+
const calls = ts.length;
|
|
150
|
+
const pressure = Math.min(1, calls / (BUDGETS[tier] || 364));
|
|
151
|
+
result[provider][tier] = { calls, pressure };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return result;
|
|
155
|
+
} catch {
|
|
156
|
+
return {
|
|
157
|
+
claude: { think: { calls: 0, pressure: 0 }, execute: { calls: 0, pressure: 0 }, search: { calls: 0, pressure: 0 } },
|
|
158
|
+
openai: { think: { calls: 0, pressure: 0 }, execute: { calls: 0, pressure: 0 }, search: { calls: 0, pressure: 0 } },
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function loadTodayCost() {
|
|
164
|
+
try {
|
|
165
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
166
|
+
const summary = JSON.parse(readFileSync(join(__dirname, `usage-summary-${today}.json`), 'utf8'));
|
|
167
|
+
return summary.totals?.cost_estimate || 0;
|
|
168
|
+
} catch { return 0; }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function loadLastDecision() {
|
|
172
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
173
|
+
const logFile = join(__dirname, `usage-${today}.jsonl`);
|
|
174
|
+
if (!existsSync(logFile)) return null;
|
|
175
|
+
try {
|
|
176
|
+
const lines = readFileSync(logFile, 'utf8').split('\n').filter(Boolean);
|
|
177
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
178
|
+
try {
|
|
179
|
+
const e = JSON.parse(lines[i]);
|
|
180
|
+
if (e.type === 'tier_recommendation') return e;
|
|
181
|
+
} catch {}
|
|
182
|
+
}
|
|
183
|
+
} catch {}
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ─── Rendering ─────────────────────────────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
function pressureBar(p, w = 10) {
|
|
190
|
+
const filled = Math.min(w, Math.round(p * w));
|
|
191
|
+
const bar = '▓'.repeat(filled) + '░'.repeat(w - filled);
|
|
192
|
+
const pct = String(Math.round(p * 100)).padStart(3) + '%';
|
|
193
|
+
let stateEmoji, stateLabel;
|
|
194
|
+
if (p >= 0.95) { stateEmoji = '🛑'; stateLabel = c(A.red + A.bold, 'throttled'); }
|
|
195
|
+
else if (p >= 0.82) { stateEmoji = '🔥'; stateLabel = c(A.red, 'hot'); }
|
|
196
|
+
else if (p >= 0.65) { stateEmoji = '🟡'; stateLabel = c(A.yellow, 'warm'); }
|
|
197
|
+
else { stateEmoji = '🟢'; stateLabel = c(A.green, 'healthy'); }
|
|
198
|
+
const barColored = p >= 0.82 ? c(A.red, bar) : p >= 0.65 ? c(A.yellow, bar) : c(A.green, bar);
|
|
199
|
+
return `${barColored} ${pct} ${stateEmoji} ${stateLabel}`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function renderDashboard(state) {
|
|
203
|
+
const { profile, providers, pressure, cost, flash } = state;
|
|
204
|
+
const pf = PROFILES[profile.name];
|
|
205
|
+
const time = new Date().toLocaleTimeString('en-US', { hour12: false });
|
|
206
|
+
const mode = (providers.claude.authed && providers.codex.authed) ? '🧠 Dual brain active' :
|
|
207
|
+
providers.claude.authed ? '🟠 Claude only' :
|
|
208
|
+
providers.codex.authed ? '🟢 OpenAI only' : '🔎 No providers';
|
|
209
|
+
|
|
210
|
+
const lines = [];
|
|
211
|
+
lines.push('');
|
|
212
|
+
lines.push(c(A.bold, ` 🧠 Dual-Brain Control Panel v${VERSION}`) + ` ${c(A.green, '🟢 Live')} ${c(A.dim, time)}`);
|
|
213
|
+
lines.push('');
|
|
214
|
+
lines.push(` ${mode}`);
|
|
215
|
+
lines.push(` 🎛️ Profile ${pf.emoji} ${c(A.bold, pf.label)} ${c(A.dim, pf.desc)}`);
|
|
216
|
+
lines.push(` 💵 Budget Session $${cost.toFixed(2)} / $${profile.budgets.session_limit_usd} Daily / $${profile.budgets.daily_limit_usd}`);
|
|
217
|
+
lines.push(` 🛡️ Gate Reviews ${profile.gate.sensitivity_floor}+ Dual-brain ${profile.gate.dual_brain_minimum}+`);
|
|
218
|
+
lines.push('');
|
|
219
|
+
|
|
220
|
+
lines.push(` 🔌 ${c(A.bold, 'Providers')}`);
|
|
221
|
+
const cStatus = providers.claude.authed ? '✅ authenticated' : '⚠️ not authenticated';
|
|
222
|
+
const xStatus = providers.codex.authed ? '✅ authenticated' : providers.codex.installed ? '⚠️ login needed' : '❌ not found';
|
|
223
|
+
lines.push(` 🟠 Claude ${cStatus} ${c(A.dim, providers.claude.models)}`);
|
|
224
|
+
lines.push(` 🟢 Codex ${xStatus} ${c(A.dim, providers.codex.models)}`);
|
|
225
|
+
lines.push('');
|
|
226
|
+
|
|
227
|
+
lines.push(` 🌡️ ${c(A.bold, 'Pressure')} ${c(A.dim, '— rolling 5h')}`);
|
|
228
|
+
for (const [label, emoji, key] of [['Claude', '🟠', 'claude'], ['OpenAI', '🟢', 'openai']]) {
|
|
229
|
+
lines.push(` ${emoji} ${label}`);
|
|
230
|
+
for (const tier of ['think', 'execute', 'search']) {
|
|
231
|
+
const p = pressure[key]?.[tier] || { pressure: 0 };
|
|
232
|
+
const tierLabel = (tier.charAt(0).toUpperCase() + tier.slice(1)).padEnd(8);
|
|
233
|
+
lines.push(` ${c(A.dim, tierLabel)} ${pressureBar(p.pressure)}`);
|
|
234
|
+
}
|
|
235
|
+
if (key === 'claude') lines.push('');
|
|
236
|
+
}
|
|
237
|
+
lines.push('');
|
|
238
|
+
|
|
239
|
+
if (flash) {
|
|
240
|
+
lines.push(` ${flash}`);
|
|
241
|
+
lines.push('');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
lines.push(c(A.dim, ' ─'.repeat(30)));
|
|
245
|
+
lines.push(` ⌨️ ${c(A.bold, '1')} Balanced ${c(A.bold, '2')} Cost-saver ${c(A.bold, '3')} Quality-first ${c(A.bold, 'b')} Budget ${c(A.bold, 'e')} Explain ${c(A.bold, 'q')} Quit`);
|
|
246
|
+
lines.push('');
|
|
247
|
+
|
|
248
|
+
return lines.join('\n');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function renderExplain(decision, profile) {
|
|
252
|
+
const lines = [];
|
|
253
|
+
lines.push('');
|
|
254
|
+
lines.push(c(A.bold, ' 🧭 Last Routing Decision'));
|
|
255
|
+
lines.push(c(A.dim, ' ' + '─'.repeat(40)));
|
|
256
|
+
|
|
257
|
+
if (!decision) {
|
|
258
|
+
lines.push(' 💤 No routing decisions recorded today.');
|
|
259
|
+
lines.push('');
|
|
260
|
+
lines.push(c(A.dim, ' Press any key to go back'));
|
|
261
|
+
return lines.join('\n');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const time = decision.timestamp?.slice(11, 19) || '??:??:??';
|
|
265
|
+
const followed = decision.followed;
|
|
266
|
+
lines.push(` 🕐 Time ${time}`);
|
|
267
|
+
lines.push(` 🔎 Detected ${decision.detected_tier || 'unknown'} tier`);
|
|
268
|
+
lines.push(` 🧠 Recommended ${decision.recommended_model || 'unknown'}`);
|
|
269
|
+
lines.push(` 🎯 Actual ${decision.actual_model || 'unknown'}`);
|
|
270
|
+
lines.push(` ${followed ? '✅' : '⚠️'} Followed ${followed ? 'yes' : 'no'}`);
|
|
271
|
+
lines.push(` 🎛️ Profile ${profile.name}`);
|
|
272
|
+
lines.push('');
|
|
273
|
+
|
|
274
|
+
if (followed) {
|
|
275
|
+
lines.push(' ✅ Routing matched the recommendation.');
|
|
276
|
+
} else {
|
|
277
|
+
lines.push(' ⚠️ Recommendation was overridden.');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
lines.push('');
|
|
281
|
+
lines.push(c(A.dim, ' Press any key to go back'));
|
|
282
|
+
return lines.join('\n');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function renderBudgetEditor(sessionVal, dailyVal, field, flash) {
|
|
286
|
+
const lines = [];
|
|
287
|
+
lines.push('');
|
|
288
|
+
lines.push(c(A.bold, ' 💵 Edit Budget'));
|
|
289
|
+
lines.push(c(A.dim, ' ' + '─'.repeat(40)));
|
|
290
|
+
lines.push('');
|
|
291
|
+
|
|
292
|
+
const sCursor = field === 'session' ? '_' : '';
|
|
293
|
+
const dCursor = field === 'daily' ? '_' : '';
|
|
294
|
+
lines.push(` Session limit: $${sessionVal}${sCursor}${field === 'session' ? c(A.dim, ' ← editing') : ''}`);
|
|
295
|
+
lines.push(` Daily limit: $${dailyVal}${dCursor}${field === 'daily' ? c(A.dim, ' ← editing') : ''}`);
|
|
296
|
+
lines.push('');
|
|
297
|
+
|
|
298
|
+
if (flash) {
|
|
299
|
+
lines.push(` ${flash}`);
|
|
300
|
+
lines.push('');
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
lines.push(c(A.dim, ' Type numbers · Tab next · Enter save · Esc cancel'));
|
|
304
|
+
return lines.join('\n');
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ─── Static (non-TTY) Output ───────────────────────────────────────────────
|
|
308
|
+
|
|
309
|
+
function renderStatic() {
|
|
310
|
+
const profile = loadProfile();
|
|
311
|
+
const providers = detectProviders();
|
|
312
|
+
const pressure = loadPressure();
|
|
313
|
+
const cost = loadTodayCost();
|
|
314
|
+
const state = { profile, providers, pressure, cost, flash: null };
|
|
315
|
+
console.log(renderDashboard(state));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ─── Interactive TUI ───────────────────────────────────────────────────────
|
|
319
|
+
|
|
320
|
+
function startTUI() {
|
|
321
|
+
let view = 'dashboard';
|
|
322
|
+
let flash = null;
|
|
323
|
+
let flashTimeout = null;
|
|
324
|
+
let refreshTimer = null;
|
|
325
|
+
|
|
326
|
+
// Budget editor state
|
|
327
|
+
let budgetSession = '';
|
|
328
|
+
let budgetDaily = '';
|
|
329
|
+
let budgetField = 'session';
|
|
330
|
+
|
|
331
|
+
function setFlash(msg, ms = 3000) {
|
|
332
|
+
flash = msg;
|
|
333
|
+
clearTimeout(flashTimeout);
|
|
334
|
+
flashTimeout = setTimeout(() => { flash = null; render(); }, ms);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function loadState() {
|
|
338
|
+
return {
|
|
339
|
+
profile: loadProfile(),
|
|
340
|
+
providers: detectProviders(),
|
|
341
|
+
pressure: loadPressure(),
|
|
342
|
+
cost: loadTodayCost(),
|
|
343
|
+
flash,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function render() {
|
|
348
|
+
let screen;
|
|
349
|
+
if (view === 'dashboard') {
|
|
350
|
+
screen = renderDashboard(loadState());
|
|
351
|
+
} else if (view === 'explain') {
|
|
352
|
+
const decision = loadLastDecision();
|
|
353
|
+
const profile = loadProfile();
|
|
354
|
+
screen = renderExplain(decision, profile);
|
|
355
|
+
} else if (view === 'budget') {
|
|
356
|
+
screen = renderBudgetEditor(budgetSession, budgetDaily, budgetField, flash);
|
|
357
|
+
}
|
|
358
|
+
process.stdout.write(A.home + A.clear + screen);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function startRefresh() {
|
|
362
|
+
stopRefresh();
|
|
363
|
+
refreshTimer = setInterval(render, 2000);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function stopRefresh() {
|
|
367
|
+
if (refreshTimer) { clearInterval(refreshTimer); refreshTimer = null; }
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function cleanup() {
|
|
371
|
+
stopRefresh();
|
|
372
|
+
clearTimeout(flashTimeout);
|
|
373
|
+
process.stdin.setRawMode(false);
|
|
374
|
+
process.stdout.write(A.reset + A.show + A.altOff);
|
|
375
|
+
process.exit(0);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function switchProfile(name) {
|
|
379
|
+
let customOverrides = null;
|
|
380
|
+
try {
|
|
381
|
+
const existing = JSON.parse(readFileSync(PROFILE_FILE, 'utf8'));
|
|
382
|
+
if (existing.custom_overrides?.budgets) customOverrides = { budgets: existing.custom_overrides.budgets };
|
|
383
|
+
} catch {}
|
|
384
|
+
saveProfile(name, customOverrides);
|
|
385
|
+
const pf = PROFILES[name];
|
|
386
|
+
setFlash(`✅ Profile switched: ${pf.emoji} ${pf.label}`);
|
|
387
|
+
render();
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Setup
|
|
391
|
+
process.stdout.write(A.altOn + A.hide);
|
|
392
|
+
readline.emitKeypressEvents(process.stdin);
|
|
393
|
+
process.stdin.setRawMode(true);
|
|
394
|
+
process.stdin.resume();
|
|
395
|
+
|
|
396
|
+
process.on('SIGINT', cleanup);
|
|
397
|
+
process.on('SIGTERM', cleanup);
|
|
398
|
+
process.on('uncaughtException', (err) => {
|
|
399
|
+
cleanup();
|
|
400
|
+
console.error(err);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
render();
|
|
404
|
+
startRefresh();
|
|
405
|
+
|
|
406
|
+
process.stdin.on('keypress', (str, key) => {
|
|
407
|
+
if (key?.ctrl && key?.name === 'c') return cleanup();
|
|
408
|
+
|
|
409
|
+
if (view === 'budget') {
|
|
410
|
+
if (key?.name === 'escape') {
|
|
411
|
+
view = 'dashboard';
|
|
412
|
+
startRefresh();
|
|
413
|
+
render();
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
if (key?.name === 'tab') {
|
|
417
|
+
budgetField = budgetField === 'session' ? 'daily' : 'session';
|
|
418
|
+
render();
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
if (key?.name === 'return') {
|
|
422
|
+
const s = parseFloat(budgetSession);
|
|
423
|
+
const d = parseFloat(budgetDaily);
|
|
424
|
+
if (isNaN(s) || s <= 0) { setFlash('❌ Invalid session limit'); render(); return; }
|
|
425
|
+
const daily = (isNaN(d) || d <= 0) ? s * 3 : d;
|
|
426
|
+
saveBudget(s, daily);
|
|
427
|
+
view = 'dashboard';
|
|
428
|
+
startRefresh();
|
|
429
|
+
setFlash(`✅ Budget updated: Session $${s} · Daily $${daily}`);
|
|
430
|
+
render();
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
if (key?.name === 'backspace') {
|
|
434
|
+
if (budgetField === 'session') budgetSession = budgetSession.slice(0, -1);
|
|
435
|
+
else budgetDaily = budgetDaily.slice(0, -1);
|
|
436
|
+
render();
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
if (str && /[0-9.]/.test(str)) {
|
|
440
|
+
if (budgetField === 'session') budgetSession += str;
|
|
441
|
+
else budgetDaily += str;
|
|
442
|
+
render();
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (view === 'explain') {
|
|
449
|
+
view = 'dashboard';
|
|
450
|
+
startRefresh();
|
|
451
|
+
render();
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Dashboard keys
|
|
456
|
+
if (key?.name === 'q' || key?.name === 'escape') return cleanup();
|
|
457
|
+
if (str === '1') return switchProfile('balanced');
|
|
458
|
+
if (str === '2') return switchProfile('cost-saver');
|
|
459
|
+
if (str === '3') return switchProfile('quality-first');
|
|
460
|
+
if (str === 'r') { render(); return; }
|
|
461
|
+
if (str === 'e') {
|
|
462
|
+
view = 'explain';
|
|
463
|
+
stopRefresh();
|
|
464
|
+
render();
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
if (str === 'b') {
|
|
468
|
+
view = 'budget';
|
|
469
|
+
stopRefresh();
|
|
470
|
+
const profile = loadProfile();
|
|
471
|
+
budgetSession = String(profile.budgets.session_limit_usd);
|
|
472
|
+
budgetDaily = String(profile.budgets.daily_limit_usd);
|
|
473
|
+
budgetField = 'session';
|
|
474
|
+
flash = null;
|
|
475
|
+
render();
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// ─── Entry ─────────────────────────────────────────────────────────────────
|
|
482
|
+
|
|
483
|
+
const interactive = process.stdin.isTTY && process.stdout.isTTY && !process.env.CI;
|
|
484
|
+
|
|
485
|
+
if (interactive) {
|
|
486
|
+
startTUI();
|
|
487
|
+
} else {
|
|
488
|
+
renderStatic();
|
|
489
|
+
}
|
|
@@ -19,7 +19,11 @@ import { fileURLToPath } from 'url';
|
|
|
19
19
|
|
|
20
20
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
21
21
|
|
|
22
|
-
const
|
|
22
|
+
const REVIEW_PROMPT_R1 = `You are GPT-5.5 performing Round 1 of a dual-brain code review.
|
|
23
|
+
Claude (Opus) will independently review the same changes, then send you their findings
|
|
24
|
+
for a collaborative Round 2 discussion.
|
|
25
|
+
|
|
26
|
+
Review the current uncommitted changes for:
|
|
23
27
|
1. Correctness — logic errors, off-by-one, null/undefined risks
|
|
24
28
|
2. Security — injection, auth bypass, data exposure
|
|
25
29
|
3. Edge cases — what could break under unusual input
|
|
@@ -34,6 +38,24 @@ Required output:
|
|
|
34
38
|
|
|
35
39
|
Be concise. Flag only real issues, not style preferences. If the code looks good, say "LGTM" and note any minor suggestions. Output your review as plain text, not JSON.`;
|
|
36
40
|
|
|
41
|
+
const REVIEW_PROMPT_R2 = `You are GPT-5.5 in Round 2 of a collaborative code review with Claude (Opus).
|
|
42
|
+
You already reviewed this diff in Round 1. Claude has now independently reviewed the same changes.
|
|
43
|
+
This is a professional peer review dialogue — two senior engineers refining their assessment together.
|
|
44
|
+
|
|
45
|
+
Claude's review findings:
|
|
46
|
+
---CLAUDE_REVIEW---
|
|
47
|
+
|
|
48
|
+
Now respond as a peer reviewer:
|
|
49
|
+
1. CONFIRMED: Issues you both found — these are high-confidence findings
|
|
50
|
+
2. MISSED: Issues Claude caught that you missed — acknowledge them
|
|
51
|
+
3. DISAGREE: Claude's findings you think are false positives — explain why
|
|
52
|
+
4. ESCALATED: Issues that are MORE severe than either of you initially rated
|
|
53
|
+
5. VERDICT: Combined assessment — LGTM, minor issues, or blocks merge
|
|
54
|
+
|
|
55
|
+
Be direct. If Claude found something real that you missed, say so.
|
|
56
|
+
If Claude flagged something that isn't actually a problem, explain why with evidence.
|
|
57
|
+
The goal is the most accurate review, not defending your initial take.`;
|
|
58
|
+
|
|
37
59
|
function loadReviewRules() {
|
|
38
60
|
const rulesFile = resolve(__dirname, '..', 'review-rules.md');
|
|
39
61
|
try {
|
|
@@ -127,9 +149,9 @@ function exit(obj) {
|
|
|
127
149
|
|
|
128
150
|
/**
|
|
129
151
|
* Try GPT review via Codex CLI (uses ChatGPT subscription auth).
|
|
130
|
-
*
|
|
152
|
+
* Round 1: independent review. Round 2: respond to Claude's review.
|
|
131
153
|
*/
|
|
132
|
-
function tryCodexReview(diff) {
|
|
154
|
+
function tryCodexReview(diff, { round = 1, claudeReview = null } = {}) {
|
|
133
155
|
if (!CODEX_BIN) return null;
|
|
134
156
|
try {
|
|
135
157
|
spawnSync(CODEX_BIN, ['login', 'status'], {
|
|
@@ -145,7 +167,14 @@ function tryCodexReview(diff) {
|
|
|
145
167
|
? diff.slice(0, MAX_DIFF_CHARS) + '\n[truncated]'
|
|
146
168
|
: diff;
|
|
147
169
|
|
|
148
|
-
|
|
170
|
+
let basePrompt;
|
|
171
|
+
if (round === 2 && claudeReview) {
|
|
172
|
+
basePrompt = REVIEW_PROMPT_R2.replace('---CLAUDE_REVIEW---', claudeReview);
|
|
173
|
+
} else {
|
|
174
|
+
basePrompt = REVIEW_PROMPT_R1;
|
|
175
|
+
}
|
|
176
|
+
const fullPrompt = basePrompt + loadReviewRules();
|
|
177
|
+
|
|
149
178
|
const proc = spawnSync(CODEX_BIN, [
|
|
150
179
|
'exec', '--json', '--ephemeral',
|
|
151
180
|
'-c', `model="${model}"`,
|
|
@@ -159,7 +188,6 @@ function tryCodexReview(diff) {
|
|
|
159
188
|
});
|
|
160
189
|
const result = proc.stdout || '';
|
|
161
190
|
|
|
162
|
-
// Parse JSONL output, find agent_message items
|
|
163
191
|
const messages = result
|
|
164
192
|
.split('\n')
|
|
165
193
|
.filter(l => l.trim())
|
|
@@ -173,16 +201,17 @@ function tryCodexReview(diff) {
|
|
|
173
201
|
const usage = messages.find(m => m.type === 'turn.completed')?.usage;
|
|
174
202
|
|
|
175
203
|
if (agentMessages.length > 0) {
|
|
204
|
+
const reviewText = agentMessages.join('\n\n');
|
|
176
205
|
return {
|
|
177
|
-
|
|
206
|
+
round,
|
|
207
|
+
review: reviewText,
|
|
178
208
|
model,
|
|
179
209
|
auth_type: 'codex_subscription',
|
|
180
|
-
issues_found: hasIssues(
|
|
210
|
+
issues_found: hasIssues(reviewText),
|
|
181
211
|
tokens: usage || null,
|
|
182
212
|
};
|
|
183
213
|
}
|
|
184
214
|
|
|
185
|
-
// Check for errors
|
|
186
215
|
const errors = messages.filter(m => m.type === 'error' || m.type === 'turn.failed');
|
|
187
216
|
if (errors.length > 0) {
|
|
188
217
|
return {
|
|
@@ -205,7 +234,7 @@ function tryCodexReview(diff) {
|
|
|
205
234
|
/**
|
|
206
235
|
* Try GPT review via direct API call (needs OPENAI_API_KEY).
|
|
207
236
|
*/
|
|
208
|
-
async function tryApiReview(diff) {
|
|
237
|
+
async function tryApiReview(diff, { round = 1, claudeReview = null } = {}) {
|
|
209
238
|
const apiKey = process.env.OPENAI_API_KEY;
|
|
210
239
|
if (!apiKey) return null;
|
|
211
240
|
|
|
@@ -214,7 +243,14 @@ async function tryApiReview(diff) {
|
|
|
214
243
|
? diff.slice(0, MAX_DIFF_CHARS) + '\n[truncated]'
|
|
215
244
|
: diff;
|
|
216
245
|
|
|
217
|
-
|
|
246
|
+
let basePrompt;
|
|
247
|
+
if (round === 2 && claudeReview) {
|
|
248
|
+
basePrompt = REVIEW_PROMPT_R2.replace('---CLAUDE_REVIEW---', claudeReview);
|
|
249
|
+
} else {
|
|
250
|
+
basePrompt = REVIEW_PROMPT_R1;
|
|
251
|
+
}
|
|
252
|
+
const fullPrompt = basePrompt + loadReviewRules();
|
|
253
|
+
|
|
218
254
|
const controller = new AbortController();
|
|
219
255
|
const timer = setTimeout(() => controller.abort(), 30_000);
|
|
220
256
|
|
|
@@ -245,6 +281,7 @@ async function tryApiReview(diff) {
|
|
|
245
281
|
if (!text) return null;
|
|
246
282
|
|
|
247
283
|
return {
|
|
284
|
+
round,
|
|
248
285
|
review: text,
|
|
249
286
|
model,
|
|
250
287
|
auth_type: 'api_key',
|
|
@@ -256,7 +293,37 @@ async function tryApiReview(diff) {
|
|
|
256
293
|
}
|
|
257
294
|
}
|
|
258
295
|
|
|
296
|
+
function parseArgs(argv) {
|
|
297
|
+
const args = {};
|
|
298
|
+
let i = 0;
|
|
299
|
+
while (i < argv.length) {
|
|
300
|
+
const arg = argv[i];
|
|
301
|
+
if (arg.startsWith('--')) {
|
|
302
|
+
const eqIdx = arg.indexOf('=');
|
|
303
|
+
if (eqIdx !== -1) {
|
|
304
|
+
args[arg.slice(2, eqIdx)] = arg.slice(eqIdx + 1);
|
|
305
|
+
} else {
|
|
306
|
+
const key = arg.slice(2);
|
|
307
|
+
const next = argv[i + 1];
|
|
308
|
+
if (next !== undefined && !next.startsWith('--')) {
|
|
309
|
+
args[key] = next;
|
|
310
|
+
i++;
|
|
311
|
+
} else {
|
|
312
|
+
args[key] = true;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
i++;
|
|
317
|
+
}
|
|
318
|
+
return args;
|
|
319
|
+
}
|
|
320
|
+
|
|
259
321
|
async function main() {
|
|
322
|
+
const args = parseArgs(process.argv.slice(2));
|
|
323
|
+
const round = args.round ? parseInt(args.round, 10) : 1;
|
|
324
|
+
const claudeReview = args['claude-review'] || null;
|
|
325
|
+
const opts = { round, claudeReview };
|
|
326
|
+
|
|
260
327
|
// 1. Get diff
|
|
261
328
|
let diff = runGit('git diff --staged') || '';
|
|
262
329
|
if (countLines(diff) < MIN_DIFF_LINES) {
|
|
@@ -264,12 +331,11 @@ async function main() {
|
|
|
264
331
|
if (countLines(headDiff) > countLines(diff)) diff = headDiff;
|
|
265
332
|
}
|
|
266
333
|
|
|
267
|
-
// Also gather content of untracked source files
|
|
268
334
|
try {
|
|
269
335
|
const untracked = runGit('git ls-files --others --exclude-standard') || '';
|
|
270
336
|
const sourceExts = /\.(ts|tsx|js|jsx|py|rs|go|java|rb|swift|kt|mjs|cjs)$/;
|
|
271
337
|
const untrackedSrc = untracked.split('\n').filter(f => f && sourceExts.test(f));
|
|
272
|
-
for (const f of untrackedSrc.slice(0, 10)) {
|
|
338
|
+
for (const f of untrackedSrc.slice(0, 10)) {
|
|
273
339
|
const content = runGit(`git diff --no-index /dev/null "${f}"`);
|
|
274
340
|
if (content) diff += '\n' + content;
|
|
275
341
|
}
|
|
@@ -279,13 +345,36 @@ async function main() {
|
|
|
279
345
|
exit({ review: 'No significant changes to review' });
|
|
280
346
|
}
|
|
281
347
|
|
|
282
|
-
// 2. Try Codex CLI first
|
|
283
|
-
const codexResult = tryCodexReview(diff);
|
|
284
|
-
if (codexResult)
|
|
348
|
+
// 2. Try Codex CLI first
|
|
349
|
+
const codexResult = tryCodexReview(diff, opts);
|
|
350
|
+
if (codexResult) {
|
|
351
|
+
if (round === 1) {
|
|
352
|
+
codexResult.instructions = `Round 1 complete. Now:
|
|
353
|
+
1. Provide YOUR independent code review of the same changes
|
|
354
|
+
2. Then call Round 2 to send your findings back to GPT:
|
|
355
|
+
node .claude/hooks/dual-brain-review.mjs --round 2 --claude-review "<your findings>"
|
|
356
|
+
3. GPT will respond — confirming shared findings, acknowledging misses, and pushing back on false positives
|
|
357
|
+
4. You then synthesize both rounds into the final review verdict`;
|
|
358
|
+
} else {
|
|
359
|
+
codexResult.instructions = `GPT has responded to your review. Synthesize into a FINAL REVIEW:
|
|
360
|
+
- CONFIRMED findings (both found) → high confidence, must fix
|
|
361
|
+
- GPT-only findings you agree with → add to your list
|
|
362
|
+
- Your findings GPT disputed → re-evaluate honestly
|
|
363
|
+
- Final verdict: LGTM, minor issues, or blocks merge`;
|
|
364
|
+
}
|
|
365
|
+
exit(codexResult);
|
|
366
|
+
}
|
|
285
367
|
|
|
286
368
|
// 3. Try direct API
|
|
287
|
-
const apiResult = await tryApiReview(diff);
|
|
288
|
-
if (apiResult)
|
|
369
|
+
const apiResult = await tryApiReview(diff, opts);
|
|
370
|
+
if (apiResult) {
|
|
371
|
+
if (round === 1) {
|
|
372
|
+
apiResult.instructions = `Round 1 complete. Provide YOUR independent review, then call Round 2 with --round 2 --claude-review "<findings>"`;
|
|
373
|
+
} else {
|
|
374
|
+
apiResult.instructions = `Synthesize both rounds into a final review verdict.`;
|
|
375
|
+
}
|
|
376
|
+
exit(apiResult);
|
|
377
|
+
}
|
|
289
378
|
|
|
290
379
|
// 4. No GPT available
|
|
291
380
|
exit({
|
|
@@ -60,8 +60,33 @@ function findCodex() {
|
|
|
60
60
|
// Prompt builder
|
|
61
61
|
// ---------------------------------------------------------------------------
|
|
62
62
|
|
|
63
|
-
function buildGptPrompt({ question, context, files }) {
|
|
63
|
+
function buildGptPrompt({ question, context, files, round, claudePerspective }) {
|
|
64
|
+
if (round === 2 && claudePerspective) {
|
|
65
|
+
return `You are GPT-5.5 in a collaborative architectural discussion with Claude (Opus).
|
|
66
|
+
You gave your initial analysis on a question. Claude has now provided its independent perspective.
|
|
67
|
+
This is a professional dialogue — two experts refining a decision together.
|
|
68
|
+
|
|
69
|
+
Original question: ${question}
|
|
70
|
+
${context ? `\nContext: ${context}` : ''}
|
|
71
|
+
|
|
72
|
+
Claude's perspective:
|
|
73
|
+
${claudePerspective}
|
|
74
|
+
|
|
75
|
+
Now respond as a colleague, not a critic. Structure your response:
|
|
76
|
+
1. AGREEMENTS: Where Claude's analysis strengthens or confirms your thinking
|
|
77
|
+
2. PUSHBACK: Where you disagree — be specific about WHY with evidence or reasoning
|
|
78
|
+
3. NEW INSIGHTS: Anything Claude's perspective surfaced that you missed
|
|
79
|
+
4. REFINED RECOMMENDATION: Your updated recommendation incorporating both perspectives
|
|
80
|
+
5. REMAINING CONCERNS: Open questions neither of you fully resolved
|
|
81
|
+
6. CONFIDENCE DELTA: Has your confidence changed? Why?
|
|
82
|
+
|
|
83
|
+
Be direct and substantive. If Claude is right about something you got wrong, say so.
|
|
84
|
+
If you still disagree after considering their points, explain what specific evidence would change your mind.`;
|
|
85
|
+
}
|
|
86
|
+
|
|
64
87
|
return `You are GPT-5.5, providing an independent architectural perspective.
|
|
88
|
+
This is Round 1 of a dual-brain analysis — Claude (Opus) will independently analyze the same question,
|
|
89
|
+
then send you their perspective for a collaborative discussion in Round 2.
|
|
65
90
|
|
|
66
91
|
Question: ${question}
|
|
67
92
|
${context ? `\nContext: ${context}` : ''}
|
|
@@ -165,7 +190,7 @@ function logUsage({ durationMs, usage, success }) {
|
|
|
165
190
|
// Core exported function
|
|
166
191
|
// ---------------------------------------------------------------------------
|
|
167
192
|
|
|
168
|
-
export async function dualThink({ question, context, files } = {}) {
|
|
193
|
+
export async function dualThink({ question, context, files, round, claudePerspective } = {}) {
|
|
169
194
|
if (!question) {
|
|
170
195
|
return {
|
|
171
196
|
gpt: null,
|
|
@@ -174,6 +199,8 @@ export async function dualThink({ question, context, files } = {}) {
|
|
|
174
199
|
};
|
|
175
200
|
}
|
|
176
201
|
|
|
202
|
+
const effectiveRound = (round === 2 && claudePerspective) ? 2 : 1;
|
|
203
|
+
|
|
177
204
|
const codexBin = findCodex();
|
|
178
205
|
if (!codexBin) {
|
|
179
206
|
return {
|
|
@@ -183,7 +210,6 @@ export async function dualThink({ question, context, files } = {}) {
|
|
|
183
210
|
};
|
|
184
211
|
}
|
|
185
212
|
|
|
186
|
-
// Check Codex auth before running
|
|
187
213
|
try {
|
|
188
214
|
execSync(`${codexBin} login status`, {
|
|
189
215
|
encoding: 'utf8',
|
|
@@ -198,7 +224,7 @@ export async function dualThink({ question, context, files } = {}) {
|
|
|
198
224
|
};
|
|
199
225
|
}
|
|
200
226
|
|
|
201
|
-
const prompt = buildGptPrompt({ question, context, files });
|
|
227
|
+
const prompt = buildGptPrompt({ question, context, files, round: effectiveRound, claudePerspective });
|
|
202
228
|
const raw = runGptAnalysis(codexBin, prompt);
|
|
203
229
|
|
|
204
230
|
logUsage({ durationMs: raw.durationMs, usage: raw.usage, success: raw.success });
|
|
@@ -207,18 +233,44 @@ export async function dualThink({ question, context, files } = {}) {
|
|
|
207
233
|
return {
|
|
208
234
|
gpt: null,
|
|
209
235
|
error: raw.error || 'GPT analysis failed',
|
|
210
|
-
fallback:
|
|
236
|
+
fallback: effectiveRound === 2
|
|
237
|
+
? 'GPT rebuttal unavailable — synthesize from Round 1 analysis alone'
|
|
238
|
+
: 'Proceed with single-brain analysis on Claude Opus',
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (effectiveRound === 2) {
|
|
243
|
+
return {
|
|
244
|
+
round: 2,
|
|
245
|
+
gpt: {
|
|
246
|
+
rebuttal: raw.text,
|
|
247
|
+
model: MODEL,
|
|
248
|
+
durationMs: raw.durationMs,
|
|
249
|
+
tokens: raw.usage,
|
|
250
|
+
},
|
|
251
|
+
instructions: `GPT has responded to your analysis. Now synthesize both rounds into a FINAL DECISION:
|
|
252
|
+
1. Where you both agree → high confidence, proceed
|
|
253
|
+
2. Where GPT pushed back on your points → re-evaluate honestly
|
|
254
|
+
3. Where you still disagree → state why and what evidence would resolve it
|
|
255
|
+
4. Final recommendation with combined confidence level`,
|
|
256
|
+
question,
|
|
211
257
|
};
|
|
212
258
|
}
|
|
213
259
|
|
|
214
260
|
return {
|
|
261
|
+
round: 1,
|
|
215
262
|
gpt: {
|
|
216
263
|
recommendation: raw.text,
|
|
217
264
|
model: MODEL,
|
|
218
265
|
durationMs: raw.durationMs,
|
|
219
266
|
tokens: raw.usage,
|
|
220
267
|
},
|
|
221
|
-
instructions:
|
|
268
|
+
instructions: `Round 1 complete. Now:
|
|
269
|
+
1. Provide YOUR independent analysis of the same question (same structure: recommendation, rationale, alternatives, risks, confidence, verification)
|
|
270
|
+
2. Then call Round 2 to send your perspective back to GPT:
|
|
271
|
+
node .claude/hooks/dual-brain-think.mjs --question "<same question>" --round 2 --claude-says "<your analysis summary>"
|
|
272
|
+
3. GPT will respond to your specific points — agreements, pushback, and refined recommendation
|
|
273
|
+
4. You then synthesize both rounds into the final decision`,
|
|
222
274
|
question,
|
|
223
275
|
context: context || null,
|
|
224
276
|
};
|
|
@@ -268,32 +320,41 @@ function printResult(result, question) {
|
|
|
268
320
|
const TOP = '╔══════════════════════════════════════════════════╗';
|
|
269
321
|
const BOT = '╚══════════════════════════════════════════════════╝';
|
|
270
322
|
|
|
323
|
+
const roundLabel = result.round === 2 ? 'Round 2 — Rebuttal' : 'Round 1 — Initial';
|
|
324
|
+
|
|
271
325
|
console.log(TOP);
|
|
272
|
-
console.log(
|
|
326
|
+
console.log(`║ 🧠 Dual-Brain Think · ${roundLabel}`.padEnd(51) + '║');
|
|
273
327
|
console.log(BAR);
|
|
274
|
-
// Truncate question to fit the box
|
|
275
328
|
const q = question.length > 44 ? question.slice(0, 41) + '...' : question;
|
|
276
329
|
console.log(`║ Question: ${q.padEnd(38)} ║`);
|
|
277
330
|
console.log(BAR);
|
|
278
331
|
|
|
279
332
|
if (!result.gpt) {
|
|
280
|
-
|
|
281
|
-
console.log(`║ ERROR: ${(result.error || 'Unknown error').padEnd(41)} ║`);
|
|
333
|
+
console.log(`║ ❌ ${(result.error || 'Unknown error').padEnd(45)} ║`);
|
|
282
334
|
console.log(BAR);
|
|
283
|
-
console.log(`║
|
|
335
|
+
console.log(`║ ↩️ ${(result.fallback || '').padEnd(45)} ║`);
|
|
284
336
|
console.log(BOT);
|
|
285
337
|
return;
|
|
286
338
|
}
|
|
287
339
|
|
|
288
|
-
const
|
|
289
|
-
|
|
340
|
+
const gptData = result.gpt;
|
|
341
|
+
const durSec = (gptData.durationMs / 1000).toFixed(1);
|
|
342
|
+
console.log(`║ 🤖 GPT-5.5 (${durSec}s):`.padEnd(51) + '║');
|
|
290
343
|
console.log(BAR);
|
|
291
344
|
console.log('');
|
|
292
|
-
console.log(
|
|
345
|
+
console.log(gptData.recommendation || gptData.rebuttal);
|
|
293
346
|
console.log('');
|
|
294
347
|
console.log(BAR);
|
|
295
|
-
|
|
296
|
-
|
|
348
|
+
|
|
349
|
+
if (result.round === 2) {
|
|
350
|
+
console.log('║ 🔄 Synthesize both rounds into final decision. ║');
|
|
351
|
+
console.log('║ Where you agree → high confidence. ║');
|
|
352
|
+
console.log('║ Where you disagree → state what would resolve it.║');
|
|
353
|
+
} else {
|
|
354
|
+
console.log('║ 📝 Your turn: analyze independently, then call ║');
|
|
355
|
+
console.log('║ Round 2 with --round 2 --claude-says "..." ║');
|
|
356
|
+
console.log('║ for GPT\'s rebuttal to your analysis. ║');
|
|
357
|
+
}
|
|
297
358
|
console.log(BOT);
|
|
298
359
|
}
|
|
299
360
|
|
|
@@ -306,7 +367,8 @@ if (import.meta.url === `file://${process.argv[1]}`) {
|
|
|
306
367
|
|
|
307
368
|
if (!args.question) {
|
|
308
369
|
console.error(
|
|
309
|
-
'Usage: node dual-brain-think.mjs --question "<question>" [--context "<
|
|
370
|
+
'Usage: node dual-brain-think.mjs --question "<question>" [--context "<ctx>"] [--files f1,f2]\n' +
|
|
371
|
+
' node dual-brain-think.mjs --question "<question>" --round 2 --claude-says "<analysis>"'
|
|
310
372
|
);
|
|
311
373
|
process.exit(1);
|
|
312
374
|
}
|
|
@@ -315,6 +377,8 @@ if (import.meta.url === `file://${process.argv[1]}`) {
|
|
|
315
377
|
question: args.question,
|
|
316
378
|
context: args.context,
|
|
317
379
|
files: args.files,
|
|
380
|
+
round: args.round ? parseInt(args.round, 10) : 1,
|
|
381
|
+
claudePerspective: args['claude-says'] || null,
|
|
318
382
|
});
|
|
319
383
|
|
|
320
384
|
printResult(result, args.question);
|
package/install.mjs
CHANGED
|
@@ -39,34 +39,34 @@ if (flag('--version') || flag('-v')) {
|
|
|
39
39
|
|
|
40
40
|
if (flag('--help') || flag('-h')) {
|
|
41
41
|
console.log(`
|
|
42
|
-
dual-brain v${VERSION} — Dual-provider orchestrator for Claude Code
|
|
42
|
+
🧠 dual-brain v${VERSION} — Dual-provider orchestrator for Claude Code
|
|
43
43
|
|
|
44
44
|
Usage: npx -y dual-brain [command] [options]
|
|
45
45
|
|
|
46
|
-
Commands:
|
|
47
|
-
(none) Auto-detect and install/update orchestrator
|
|
48
|
-
status
|
|
49
|
-
mode Show or switch profile
|
|
50
|
-
budget Set session/daily spend limits
|
|
51
|
-
explain
|
|
52
|
-
init Alias for default install
|
|
46
|
+
⌨️ Commands:
|
|
47
|
+
(none) 🧠 Auto-detect and install/update orchestrator
|
|
48
|
+
status 🟢 Open live control panel
|
|
49
|
+
mode 🎛️ Show or switch profile
|
|
50
|
+
budget 💵 Set session/daily spend limits
|
|
51
|
+
explain 🧭 Explain last routing decision
|
|
52
|
+
init Alias for default install
|
|
53
53
|
|
|
54
54
|
Options:
|
|
55
|
-
--force Overwrite all existing config
|
|
56
|
-
--dry-run Detect environment only
|
|
57
|
-
--json Output detection as JSON
|
|
55
|
+
--force Overwrite all existing config
|
|
56
|
+
--dry-run Detect environment only
|
|
57
|
+
--json Output detection as JSON
|
|
58
58
|
--help Show this help
|
|
59
59
|
|
|
60
|
-
Profiles:
|
|
61
|
-
balanced Standard routing — best model
|
|
62
|
-
cost-saver Minimize spend — prefer cheaper models
|
|
63
|
-
quality-first Maximum quality — dual-brain for medium+
|
|
60
|
+
🎛️ Profiles:
|
|
61
|
+
⚖️ balanced Standard routing — best model per tier
|
|
62
|
+
💸 cost-saver Minimize spend — prefer cheaper models
|
|
63
|
+
💎 quality-first Maximum quality — dual-brain for medium+
|
|
64
64
|
|
|
65
|
-
Examples:
|
|
65
|
+
🚀 Examples:
|
|
66
66
|
${cmd('npx dual-brain')} # install or update
|
|
67
|
-
${cmd('npx dual-brain status')} #
|
|
67
|
+
${cmd('npx dual-brain status')} # open control panel
|
|
68
68
|
${cmd('npx dual-brain mode cost-saver')} # switch profile
|
|
69
|
-
${cmd('npx dual-brain budget 8 25')} #
|
|
69
|
+
${cmd('npx dual-brain budget 8 25')} # \$8 session / \$25 daily
|
|
70
70
|
${cmd('npx dual-brain explain')} # last routing decision
|
|
71
71
|
`);
|
|
72
72
|
process.exit(0);
|
|
@@ -381,43 +381,50 @@ function install(workspace, env, mode) {
|
|
|
381
381
|
|
|
382
382
|
// ─── Status Report ──────────────────────────────────────────────────────────
|
|
383
383
|
|
|
384
|
-
function statusIcon(val) { return val ? '
|
|
384
|
+
function statusIcon(val) { return val ? '✅' : '❌'; }
|
|
385
|
+
|
|
386
|
+
const MODE_EMOJIS = {
|
|
387
|
+
'dual': '🧠',
|
|
388
|
+
'claude-only': '🟠',
|
|
389
|
+
'openai-only': '🟢',
|
|
390
|
+
'detect-only': '🔎',
|
|
391
|
+
};
|
|
385
392
|
|
|
386
393
|
function printReport(env, mode, actions) {
|
|
387
394
|
const lines = [];
|
|
388
395
|
|
|
389
396
|
lines.push(br('╔', '╗'));
|
|
390
|
-
lines.push(ln(
|
|
397
|
+
lines.push(ln(`🧠 Dual-Brain Orchestrator v${VERSION}`));
|
|
391
398
|
lines.push(sep());
|
|
392
399
|
|
|
393
|
-
lines.push(ln('Environment'));
|
|
400
|
+
lines.push(ln('🌎 Environment'));
|
|
394
401
|
if (env.isReplit) {
|
|
395
|
-
lines.push(ln(` Platform:
|
|
402
|
+
lines.push(ln(` 🌀 Platform: Replit${env.hasReplitTools ? ' + replit-tools' : ''}`));
|
|
396
403
|
} else {
|
|
397
404
|
lines.push(ln(' Platform: standalone'));
|
|
398
405
|
}
|
|
399
406
|
|
|
400
407
|
const cVer = env.claude.version ? ` ${env.claude.version}` : '';
|
|
401
|
-
const cAuth = env.claude.authed ? 'authenticated' : env.claude.installed ? '
|
|
402
|
-
lines.push(ln(` Claude
|
|
408
|
+
const cAuth = env.claude.authed ? '✅ authenticated' : env.claude.installed ? '⚠️ login needed' : '❌ not found';
|
|
409
|
+
lines.push(ln(` 🟠 Claude: ${cAuth}${cVer}`));
|
|
403
410
|
|
|
404
411
|
const xVer = env.codex.version ? ` ${env.codex.version}` : '';
|
|
405
|
-
const xAuth = env.codex.authed ? 'authenticated' : env.codex.installed ? '
|
|
406
|
-
lines.push(ln(` Codex
|
|
412
|
+
const xAuth = env.codex.authed ? '✅ authenticated' : env.codex.installed ? '⚠️ login needed' : '❌ not found';
|
|
413
|
+
lines.push(ln(` 🟢 Codex: ${xAuth}${xVer}`));
|
|
407
414
|
|
|
408
415
|
lines.push(sep());
|
|
409
|
-
lines.push(ln(
|
|
416
|
+
lines.push(ln(`${MODE_EMOJIS[mode.mode] || '🧠'} Mode: ${MODE_LABELS[mode.mode]}`));
|
|
410
417
|
|
|
411
418
|
if (actions) {
|
|
412
419
|
lines.push(sep());
|
|
413
|
-
lines.push(ln('Installed'));
|
|
420
|
+
lines.push(ln('📝 Installed'));
|
|
414
421
|
for (const a of actions) lines.push(ln(` ${a}`));
|
|
415
422
|
}
|
|
416
423
|
|
|
417
424
|
const needsAction = !env.claude.authed || !env.codex.authed;
|
|
418
425
|
if (needsAction && mode.mode !== 'dual') {
|
|
419
426
|
lines.push(sep());
|
|
420
|
-
lines.push(ln('
|
|
427
|
+
lines.push(ln('🔓 Unlock full power:'));
|
|
421
428
|
if (!env.claude.installed) {
|
|
422
429
|
lines.push(ln(' curl -fsSL https://claude.ai/install.sh | sh'));
|
|
423
430
|
}
|
|
@@ -436,8 +443,8 @@ function printReport(env, mode, actions) {
|
|
|
436
443
|
lines.push(sep());
|
|
437
444
|
if (actions) {
|
|
438
445
|
lines.push(ln(mode.mode === 'dual'
|
|
439
|
-
? 'Ready
|
|
440
|
-
: 'Ready
|
|
446
|
+
? '✅ Ready: both providers active, no restart needed'
|
|
447
|
+
: '✅ Ready: hooks active, run commands above for full power'));
|
|
441
448
|
} else {
|
|
442
449
|
lines.push(ln('Dry run — no files written'));
|
|
443
450
|
}
|
|
@@ -448,35 +455,26 @@ function printReport(env, mode, actions) {
|
|
|
448
455
|
console.log('');
|
|
449
456
|
|
|
450
457
|
if (actions) {
|
|
451
|
-
console.log(' What
|
|
452
|
-
console.log(' Every Claude Code session
|
|
453
|
-
console.log('
|
|
454
|
-
console.log('
|
|
458
|
+
console.log(' 🧭 What changed:');
|
|
459
|
+
console.log(' Every Claude Code session now auto-routes agent work by');
|
|
460
|
+
console.log(' complexity — cheap models for search, mid-tier for execution,');
|
|
461
|
+
console.log(' best models for thinking. Cost is tracked automatically.');
|
|
455
462
|
if (mode.mode === 'dual') {
|
|
456
|
-
console.log(' Both Claude and GPT are available as work providers.');
|
|
463
|
+
console.log(' 🧠 Both Claude and GPT are available as work providers.');
|
|
457
464
|
}
|
|
458
465
|
console.log('');
|
|
459
|
-
|
|
460
|
-
|
|
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
|
-
}
|
|
466
|
+
console.log(' ⌨️ Open the control panel:');
|
|
467
|
+
console.log(` ${cmd('npx dual-brain status')}`);
|
|
470
468
|
console.log('');
|
|
471
|
-
console.log(' In-session tools (ask Claude to run
|
|
469
|
+
console.log(' 🩺 In-session tools (ask Claude to run):');
|
|
472
470
|
console.log(' node .claude/hooks/health-check.mjs # verify setup');
|
|
473
471
|
console.log(' node .claude/hooks/cost-report.mjs # see activity');
|
|
474
|
-
console.log(' node .claude/hooks/
|
|
472
|
+
console.log(' node .claude/hooks/decision-ledger.mjs # routing insights');
|
|
475
473
|
if (mode.openaiEnabled) {
|
|
476
474
|
console.log(' node .claude/hooks/dual-brain-review.mjs # GPT code review');
|
|
477
475
|
}
|
|
478
476
|
console.log('');
|
|
479
|
-
console.log(' Customize:');
|
|
477
|
+
console.log(' ⚙️ Customize:');
|
|
480
478
|
console.log(' .claude/review-rules.md # your project\'s review rules');
|
|
481
479
|
console.log(' .claude/orchestrator.json # routing, budgets, tiers');
|
|
482
480
|
console.log('');
|
|
@@ -616,12 +614,13 @@ function cmdMode() {
|
|
|
616
614
|
|
|
617
615
|
if (!modeArg || modeArg === 'list') {
|
|
618
616
|
const current = loadProfile(workspace);
|
|
617
|
+
const PEMOJIS = { balanced: '⚖️ ', 'cost-saver': '💸', 'quality-first': '💎' };
|
|
619
618
|
console.log('');
|
|
620
|
-
console.log('
|
|
619
|
+
console.log(' 🎛️ Profiles:');
|
|
621
620
|
console.log('');
|
|
622
621
|
for (const [name, p] of Object.entries(PROFILES)) {
|
|
623
|
-
const active = name === current.name ? '
|
|
624
|
-
console.log(` ${name.padEnd(15)} ${p.description}${active}`);
|
|
622
|
+
const active = name === current.name ? ' ✅ active' : '';
|
|
623
|
+
console.log(` ${PEMOJIS[name] || ' '} ${name.padEnd(15)} ${p.description}${active}`);
|
|
625
624
|
}
|
|
626
625
|
console.log('');
|
|
627
626
|
console.log(` Switch: ${cmd('npx dual-brain mode <profile>')}`);
|
|
@@ -647,17 +646,18 @@ function cmdMode() {
|
|
|
647
646
|
|
|
648
647
|
saveProfile(workspace, modeArg, customOverrides);
|
|
649
648
|
|
|
649
|
+
const PEMOJIS = { balanced: '⚖️ ', 'cost-saver': '💸', 'quality-first': '💎' };
|
|
650
650
|
console.log('');
|
|
651
|
-
console.log(` Profile switched
|
|
651
|
+
console.log(` ✅ Profile switched: ${PEMOJIS[modeArg] || ''} ${modeArg}`);
|
|
652
652
|
console.log(` ${profile.description}`);
|
|
653
653
|
console.log('');
|
|
654
|
-
console.log('
|
|
655
|
-
console.log(`
|
|
656
|
-
console.log(` Budget:
|
|
657
|
-
console.log(` Reviews
|
|
658
|
-
console.log(` Dual-brain:
|
|
654
|
+
console.log(' 🧭 Routing changes:');
|
|
655
|
+
console.log(` Provider: ${profile.routing.prefer_provider}`);
|
|
656
|
+
console.log(` 💵 Budget: $${profile.budgets.session_limit_usd}/session, $${profile.budgets.daily_limit_usd}/day`);
|
|
657
|
+
console.log(` 🛡️ Reviews: ${profile.quality_gate.sensitivity_floor} risk+`);
|
|
658
|
+
console.log(` 🧠 Dual-brain: ${profile.quality_gate.dual_brain_minimum} risk+`);
|
|
659
659
|
console.log('');
|
|
660
|
-
console.log(' Active immediately
|
|
660
|
+
console.log(' 🟢 Active immediately, no restart needed.');
|
|
661
661
|
console.log('');
|
|
662
662
|
}
|
|
663
663
|
|
|
@@ -671,9 +671,9 @@ function cmdBudget() {
|
|
|
671
671
|
if (sessionArg == null) {
|
|
672
672
|
const profile = loadProfile(workspace);
|
|
673
673
|
console.log('');
|
|
674
|
-
console.log(' Current budget
|
|
675
|
-
console.log(` Session:
|
|
676
|
-
console.log(` Daily:
|
|
674
|
+
console.log(' 💵 Current budget:');
|
|
675
|
+
console.log(` Session: ⚠️ $${profile.budgets.session_warn_usd} warn · 🛑 $${profile.budgets.session_limit_usd} limit`);
|
|
676
|
+
console.log(` Daily: ⚠️ $${profile.budgets.daily_warn_usd} warn · 🛑 $${profile.budgets.daily_limit_usd} limit`);
|
|
677
677
|
console.log('');
|
|
678
678
|
console.log(` Set limits: ${cmd('npx dual-brain budget <session$> [daily$]')}`);
|
|
679
679
|
console.log(` Example: ${cmd('npx dual-brain budget 8 25')}`);
|
|
@@ -710,11 +710,11 @@ function cmdBudget() {
|
|
|
710
710
|
renameSync(budgetTmp, budgetTarget);
|
|
711
711
|
|
|
712
712
|
console.log('');
|
|
713
|
-
console.log(' Budget
|
|
714
|
-
console.log(` Session:
|
|
715
|
-
console.log(` Daily:
|
|
713
|
+
console.log(' ✅ Budget updated:');
|
|
714
|
+
console.log(` Session: ⚠️ $${customOverrides.budgets.session_warn_usd} warn · 🛑 $${sessionArg} limit`);
|
|
715
|
+
console.log(` Daily: ⚠️ $${customOverrides.budgets.daily_warn_usd} warn · 🛑 $${daily} limit`);
|
|
716
716
|
console.log('');
|
|
717
|
-
console.log(' Active immediately
|
|
717
|
+
console.log(' 🟢 Active immediately, no restart needed.');
|
|
718
718
|
console.log('');
|
|
719
719
|
}
|
|
720
720
|
|
|
@@ -728,7 +728,7 @@ function cmdExplain() {
|
|
|
728
728
|
|
|
729
729
|
if (!existsSync(logFile)) {
|
|
730
730
|
console.log('');
|
|
731
|
-
console.log(' No routing decisions recorded today.');
|
|
731
|
+
console.log(' 💤 No routing decisions recorded today.');
|
|
732
732
|
console.log(' Start a Claude Code session and the tier enforcer will log decisions.');
|
|
733
733
|
console.log('');
|
|
734
734
|
return;
|
|
@@ -752,7 +752,7 @@ function cmdExplain() {
|
|
|
752
752
|
|
|
753
753
|
if (!lastRec) {
|
|
754
754
|
console.log('');
|
|
755
|
-
console.log(' No routing decisions found in today\'s log.');
|
|
755
|
+
console.log(' 💤 No routing decisions found in today\'s log.');
|
|
756
756
|
console.log(' The tier enforcer logs decisions when Agent tool is used.');
|
|
757
757
|
console.log('');
|
|
758
758
|
return;
|
|
@@ -761,23 +761,23 @@ function cmdExplain() {
|
|
|
761
761
|
const profile = loadProfile(workspace);
|
|
762
762
|
|
|
763
763
|
console.log('');
|
|
764
|
-
console.log(' Last Routing Decision');
|
|
764
|
+
console.log(' 🧭 Last Routing Decision');
|
|
765
765
|
console.log(' ' + '─'.repeat(40));
|
|
766
|
-
console.log(` Time:
|
|
767
|
-
console.log(` Detected:
|
|
768
|
-
console.log(` Recommended:
|
|
769
|
-
console.log(` Actual:
|
|
770
|
-
console.log(` Followed:
|
|
771
|
-
console.log(` Profile:
|
|
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(` ${lastRec.followed ? '✅' : '⚠️'} Followed: ${lastRec.followed ? 'yes' : 'no'}`);
|
|
771
|
+
console.log(` 🎛️ Profile: ${profile.name}`);
|
|
772
772
|
console.log('');
|
|
773
773
|
|
|
774
774
|
if (!lastRec.followed) {
|
|
775
|
-
console.log('
|
|
775
|
+
console.log(' ⚠️ Recommendation was overridden. This may mean:');
|
|
776
776
|
console.log(' - The task needed a different model (valid override)');
|
|
777
777
|
console.log(' - The subagent_type forced a specific tier');
|
|
778
778
|
console.log(` - Profile "${profile.name}" adjusted the threshold`);
|
|
779
779
|
} else {
|
|
780
|
-
console.log('
|
|
780
|
+
console.log(' ✅ Routing matched the recommendation.');
|
|
781
781
|
}
|
|
782
782
|
|
|
783
783
|
let total = 0, followed = 0;
|
|
@@ -796,7 +796,18 @@ function cmdExplain() {
|
|
|
796
796
|
// ─── Main ───────────────────────────────────────────────────────────────────
|
|
797
797
|
|
|
798
798
|
function main() {
|
|
799
|
-
if (subcommand === 'status')
|
|
799
|
+
if (subcommand === 'status') {
|
|
800
|
+
// Launch interactive TUI if available and TTY
|
|
801
|
+
const panelPath = join(resolve(process.cwd()), '.claude', 'hooks', 'control-panel.mjs');
|
|
802
|
+
const pkgPanel = join(__dirname, 'hooks', 'control-panel.mjs');
|
|
803
|
+
const panel = existsSync(panelPath) ? panelPath : existsSync(pkgPanel) ? pkgPanel : null;
|
|
804
|
+
if (panel && process.stdin.isTTY && process.stdout.isTTY && !process.env.CI) {
|
|
805
|
+
const { status } = spawnSync(process.execPath, [panel], { stdio: 'inherit' });
|
|
806
|
+
process.exit(status || 0);
|
|
807
|
+
}
|
|
808
|
+
cmdStatus();
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
800
811
|
if (subcommand === 'mode') { cmdMode(); return; }
|
|
801
812
|
if (subcommand === 'budget') { cmdBudget(); return; }
|
|
802
813
|
if (subcommand === 'explain') { cmdExplain(); return; }
|
package/package.json
CHANGED