dual-brain 3.2.0 → 3.4.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 +534 -0
- package/hooks/dual-brain-review.mjs +106 -17
- package/hooks/dual-brain-think.mjs +81 -17
- package/install.mjs +88 -194
- package/package.json +1 -1
|
@@ -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);
|