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.
@@ -19,7 +19,11 @@ import { fileURLToPath } from 'url';
19
19
 
20
20
  const __dirname = dirname(fileURLToPath(import.meta.url));
21
21
 
22
- const REVIEW_PROMPT = `Review the current uncommitted changes in this repo for:
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
- * Returns review text or null if codex isn't available.
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
- const fullPrompt = REVIEW_PROMPT + loadReviewRules();
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
- review: agentMessages.join('\n\n'),
206
+ round,
207
+ review: reviewText,
178
208
  model,
179
209
  auth_type: 'codex_subscription',
180
- issues_found: hasIssues(agentMessages.join(' ')),
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
- const fullPrompt = REVIEW_PROMPT + loadReviewRules();
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)) { // cap at 10 files
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 (uses ChatGPT subscription)
283
- const codexResult = tryCodexReview(diff);
284
- if (codexResult) exit(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) exit(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: 'Proceed with single-brain analysis on Claude Opus',
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: 'Now provide YOUR independent analysis of the same question. Then compare both perspectives and make a final decision. If you disagree with GPT, explain why with evidence.',
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('║ Dual-Brain Think ║');
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
- // Failure path
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(`║ Fallback: ${(result.fallback || '').padEnd(39)} ║`);
335
+ console.log(`║ ↩️ ${(result.fallback || '').padEnd(45)} ║`);
284
336
  console.log(BOT);
285
337
  return;
286
338
  }
287
339
 
288
- const durSec = (result.gpt.durationMs / 1000).toFixed(1);
289
- console.log(`║ GPT-5.5 Perspective (${MODEL}, ${durSec}s):`.padEnd(51) + '║');
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(result.gpt.recommendation);
345
+ console.log(gptData.recommendation || gptData.rebuttal);
293
346
  console.log('');
294
347
  console.log(BAR);
295
- console.log('║ Now: Provide YOUR analysis and compare. ║');
296
- console.log('║ If you disagree, explain why with evidence. ║');
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 "<context>"] [--files file1,file2]'
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);