dual-brain 4.2.0 → 4.5.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.
@@ -5,11 +5,24 @@
5
5
  * Sends git diffs to GPT for independent code review using the Codex CLI
6
6
  * (uses your ChatGPT subscription — no API key needed).
7
7
  *
8
+ * Auto mode (default — no --round flag):
9
+ * Runs the full 2-round review collaboration automatically.
10
+ * node .claude/hooks/dual-brain-review.mjs
11
+ *
12
+ * Manual Round 1:
13
+ * node .claude/hooks/dual-brain-review.mjs --round 1
14
+ *
15
+ * Manual Round 2:
16
+ * node .claude/hooks/dual-brain-review.mjs --round 2 --claude-review "<findings>"
17
+ *
18
+ * Force manual mode:
19
+ * node .claude/hooks/dual-brain-review.mjs --manual
20
+ *
8
21
  * Falls back to direct OpenAI API if OPENAI_API_KEY is set.
9
22
  * Falls back to "no GPT available" if neither works.
10
23
  *
11
- * Usage: node .claude/hooks/dual-brain-review.mjs
12
- * Output: JSON to stdout — always valid, never crashes.
24
+ * Output: JSON to stdout — always valid, never crashes (manual/round mode).
25
+ * Human-readable output in auto mode.
13
26
  */
14
27
 
15
28
  import { execSync, spawnSync } from 'child_process';
@@ -56,6 +69,16 @@ Be direct. If Claude found something real that you missed, say so.
56
69
  If Claude flagged something that isn't actually a problem, explain why with evidence.
57
70
  The goal is the most accurate review, not defending your initial take.`;
58
71
 
72
+ const CLAUDE_REVIEW_PROMPT = `Review the current git diff for bugs, security issues, and code quality problems.
73
+
74
+ Look for:
75
+ 1. Correctness — logic errors, null/undefined risks, off-by-one
76
+ 2. Security — injection, auth bypass, data exposure
77
+ 3. Edge cases — what breaks under unusual input
78
+ 4. Quality — naming issues, unnecessary complexity
79
+
80
+ Be concise — under 300 words. List findings ordered by severity. If the code looks good, say LGTM.`;
81
+
59
82
  function loadReviewRules() {
60
83
  const rulesFile = resolve(__dirname, '..', 'review-rules.md');
61
84
  try {
@@ -70,6 +93,7 @@ function loadReviewRules() {
70
93
  const MAX_DIFF_CHARS = 15000;
71
94
  const MIN_DIFF_LINES = 5;
72
95
  const CODEX_TIMEOUT = 90;
96
+ const CLAUDE_TIMEOUT_MS = 60_000;
73
97
 
74
98
  function findCodex() {
75
99
  const candidates = [
@@ -94,6 +118,26 @@ function findCodex() {
94
118
  return null;
95
119
  }
96
120
 
121
+ function findClaude() {
122
+ try {
123
+ const which = spawnSync('which', ['claude'], { encoding: 'utf8', stdio: 'pipe', timeout: 3000 });
124
+ if (which.status === 0 && which.stdout.trim()) return which.stdout.trim();
125
+ } catch {}
126
+ const home = process.env.HOME || process.env.USERPROFILE || '';
127
+ const fallbacks = [
128
+ join(home, '.local', 'bin', 'claude'),
129
+ join(home, 'bin', 'claude'),
130
+ '/usr/local/bin/claude',
131
+ ];
132
+ for (const p of fallbacks) {
133
+ try {
134
+ const res = spawnSync(p, ['--version'], { stdio: 'pipe', timeout: 3000 });
135
+ if (res.status === 0) return p;
136
+ } catch {}
137
+ }
138
+ return null;
139
+ }
140
+
97
141
  const CODEX_BIN = findCodex();
98
142
 
99
143
  function runGit(cmd) {
@@ -142,6 +186,58 @@ function hasIssues(text) {
142
186
  return true;
143
187
  }
144
188
 
189
+ function buildReviewSynthesis(gptR1Text, claudeText, gptR2Text) {
190
+ const lines = [];
191
+
192
+ lines.push('REVIEW SYNTHESIS');
193
+ lines.push('─'.repeat(50));
194
+
195
+ // CONFIRMED findings
196
+ const confirmedMatch = gptR2Text.match(/CONFIRMED[:\s\n]+([\s\S]*?)(?=\n\s*(?:MISSED|DISAGREE|ESCALATED|VERDICT|[0-9]+\.)|$)/i);
197
+ if (confirmedMatch && confirmedMatch[1].trim().length > 5) {
198
+ lines.push('');
199
+ lines.push('HIGH-CONFIDENCE FINDINGS (both found):');
200
+ lines.push(confirmedMatch[1].trim().split('\n').slice(0, 6).join('\n'));
201
+ }
202
+
203
+ // MISSED findings (Claude caught, GPT missed)
204
+ const missedMatch = gptR2Text.match(/MISSED[:\s\n]+([\s\S]*?)(?=\n\s*(?:DISAGREE|ESCALATED|VERDICT|[0-9]+\.)|$)/i);
205
+ if (missedMatch && missedMatch[1].trim().length > 5) {
206
+ lines.push('');
207
+ lines.push('ADDITIONAL FINDINGS (Claude caught):');
208
+ lines.push(missedMatch[1].trim().split('\n').slice(0, 4).join('\n'));
209
+ }
210
+
211
+ // ESCALATED
212
+ const escalatedMatch = gptR2Text.match(/ESCALATED[:\s\n]+([\s\S]*?)(?=\n\s*(?:VERDICT|[0-9]+\.)|$)/i);
213
+ if (escalatedMatch && escalatedMatch[1].trim().length > 5) {
214
+ lines.push('');
215
+ lines.push('ESCALATED SEVERITY:');
216
+ lines.push(escalatedMatch[1].trim().split('\n').slice(0, 3).join('\n'));
217
+ }
218
+
219
+ // DISAGREE
220
+ const disagreeMatch = gptR2Text.match(/DISAGREE[:\s\n]+([\s\S]*?)(?=\n\s*(?:ESCALATED|VERDICT|[0-9]+\.)|$)/i);
221
+ if (disagreeMatch && disagreeMatch[1].trim().length > 5) {
222
+ lines.push('');
223
+ lines.push('DISPUTED (possible false positives):');
224
+ lines.push(disagreeMatch[1].trim().split('\n').slice(0, 3).join('\n'));
225
+ }
226
+
227
+ // VERDICT
228
+ const verdictMatch = gptR2Text.match(/VERDICT[:\s\n]+([\s\S]*?)(?=\n\s*[0-9]+\.|$)/i);
229
+ if (verdictMatch) {
230
+ lines.push('');
231
+ lines.push('VERDICT:');
232
+ lines.push(verdictMatch[1].trim().split('\n').slice(0, 2).join('\n'));
233
+ }
234
+
235
+ lines.push('');
236
+ lines.push('─'.repeat(50));
237
+
238
+ return lines.join('\n');
239
+ }
240
+
145
241
  function exit(obj) {
146
242
  process.stdout.write(JSON.stringify(obj) + '\n');
147
243
  process.exit(0);
@@ -231,6 +327,34 @@ function tryCodexReview(diff, { round = 1, claudeReview = null } = {}) {
231
327
  }
232
328
  }
233
329
 
330
+ /**
331
+ * Try Claude CLI review.
332
+ */
333
+ function tryClaudeReview(diff) {
334
+ const claudeBin = findClaude();
335
+ if (!claudeBin) return null;
336
+
337
+ const truncated = diff.length > MAX_DIFF_CHARS
338
+ ? diff.slice(0, MAX_DIFF_CHARS) + '\n[truncated]'
339
+ : diff;
340
+
341
+ const prompt = `${CLAUDE_REVIEW_PROMPT}\n\nDiff to review:\n\`\`\`diff\n${truncated}\n\`\`\``;
342
+
343
+ try {
344
+ const proc = spawnSync(claudeBin, ['-p', prompt], {
345
+ encoding: 'utf8',
346
+ stdio: ['pipe', 'pipe', 'pipe'],
347
+ timeout: CLAUDE_TIMEOUT_MS,
348
+ });
349
+
350
+ if (proc.status === 0 && proc.stdout && proc.stdout.trim()) {
351
+ return proc.stdout.trim();
352
+ }
353
+ } catch {}
354
+
355
+ return null;
356
+ }
357
+
234
358
  /**
235
359
  * Try GPT review via direct API call (needs OPENAI_API_KEY).
236
360
  */
@@ -318,8 +442,121 @@ function parseArgs(argv) {
318
442
  return args;
319
443
  }
320
444
 
445
+ // ---------------------------------------------------------------------------
446
+ // Auto mode — full 2-round review collaboration in one shot
447
+ // ---------------------------------------------------------------------------
448
+
449
+ async function runAutoReviewMode(diff) {
450
+ const BAR = '╠══════════════════════════════════════════════════╣';
451
+ const TOP = '╔══════════════════════════════════════════════════╗';
452
+ const BOT = '╚══════════════════════════════════════════════════╝';
453
+ const WIDE = '║';
454
+
455
+ const lineCount = countLines(diff);
456
+
457
+ console.log(TOP);
458
+ console.log(`${WIDE} Dual-Brain Review — Auto Mode`.padEnd(51) + WIDE);
459
+ console.log(`${WIDE} ${lineCount} diff lines to review`.padEnd(51) + WIDE);
460
+ console.log(BOT);
461
+ console.log('');
462
+
463
+ if (!CODEX_BIN) {
464
+ console.log('[Auto mode] Codex CLI not found — falling back to manual mode.');
465
+ console.log('');
466
+ console.log('Manual steps:');
467
+ console.log(' 1. Run: node hooks/dual-brain-review.mjs --round 1');
468
+ console.log(' 2. Review independently');
469
+ console.log(' 3. Run: node hooks/dual-brain-review.mjs --round 2 --claude-review "<findings>"');
470
+ return;
471
+ }
472
+
473
+ // Step 1: GPT Round 1
474
+ console.log('[ 1/4 ] Sending diff to GPT for Round 1 review...');
475
+ const r1Result = tryCodexReview(diff, { round: 1 });
476
+
477
+ if (!r1Result || r1Result.error) {
478
+ const errMsg = r1Result?.review || 'Codex unavailable or not authenticated';
479
+ console.log(`[Auto mode] GPT Round 1 failed: ${errMsg}`);
480
+
481
+ // Try API fallback
482
+ const apiR1 = await tryApiReview(diff, { round: 1 });
483
+ if (!apiR1) {
484
+ console.log('[Auto mode] No GPT available. Falling back to manual mode.');
485
+ return;
486
+ }
487
+ console.log('');
488
+ console.log(TOP);
489
+ console.log(`${WIDE} Round 1 — GPT Review (API fallback)`.padEnd(51) + WIDE);
490
+ console.log(BOT);
491
+ console.log('');
492
+ console.log(apiR1.review);
493
+ console.log('');
494
+ // Can't continue with auto Round 2 via API easily — prompt manual
495
+ console.log('[Auto mode] API fallback: review Claude perspective manually, then run Round 2.');
496
+ return;
497
+ }
498
+
499
+ console.log('');
500
+ console.log(TOP);
501
+ console.log(`${WIDE} Round 1 — GPT Review`.padEnd(51) + WIDE);
502
+ console.log(BOT);
503
+ console.log('');
504
+ console.log(r1Result.review);
505
+ console.log('');
506
+
507
+ // Step 2: Claude's independent review
508
+ console.log('[ 2/4 ] Generating Claude independent review...');
509
+ const claudeReviewText = tryClaudeReview(diff);
510
+
511
+ if (!claudeReviewText) {
512
+ console.log('[Auto mode] Claude CLI not available — skipping Claude review step.');
513
+ console.log('Set your PATH to include the `claude` binary to enable full auto mode.');
514
+ console.log('');
515
+ } else {
516
+ console.log('');
517
+ console.log(TOP);
518
+ console.log(`${WIDE} Claude Independent Review`.padEnd(51) + WIDE);
519
+ console.log(BOT);
520
+ console.log('');
521
+ console.log(claudeReviewText);
522
+ console.log('');
523
+ }
524
+
525
+ // Step 3: GPT Round 2
526
+ const claudeFindings = claudeReviewText || '(Claude review unavailable — assess independently)';
527
+ console.log('[ 3/4 ] Sending Round 2 to GPT with Claude findings...');
528
+ const r2Result = tryCodexReview(diff, { round: 2, claudeReview: claudeFindings });
529
+
530
+ if (!r2Result || r2Result.error) {
531
+ console.log('[Auto mode] GPT Round 2 failed. Synthesis skipped.');
532
+ console.log('Review Round 1 and Claude findings above for your assessment.');
533
+ return;
534
+ }
535
+
536
+ console.log('');
537
+ console.log(TOP);
538
+ console.log(`${WIDE} Round 2 — GPT Cross-Validation`.padEnd(51) + WIDE);
539
+ console.log(BOT);
540
+ console.log('');
541
+ console.log(r2Result.review);
542
+ console.log('');
543
+
544
+ // Step 4: Synthesis
545
+ console.log('[ 4/4 ] Building review synthesis...');
546
+ console.log('');
547
+ console.log(TOP);
548
+ console.log(`${WIDE} Final Review Synthesis`.padEnd(51) + WIDE);
549
+ console.log(BOT);
550
+ console.log('');
551
+ console.log(buildReviewSynthesis(r1Result.review, claudeReviewText || '', r2Result.review));
552
+ console.log('');
553
+ }
554
+
321
555
  async function main() {
322
556
  const args = parseArgs(process.argv.slice(2));
557
+ const hasExplicitRound = args.round !== undefined;
558
+ const isManual = args.manual === true || hasExplicitRound;
559
+
323
560
  const round = args.round ? parseInt(args.round, 10) : 1;
324
561
  const claudeReview = args['claude-review'] || null;
325
562
  const opts = { round, claudeReview };
@@ -341,6 +578,16 @@ async function main() {
341
578
  }
342
579
  } catch {}
343
580
 
581
+ if (!isManual) {
582
+ // Auto mode — human-readable output when there are changes
583
+ if (countLines(diff) >= MIN_DIFF_LINES) {
584
+ await runAutoReviewMode(diff);
585
+ return;
586
+ }
587
+ // No changes: fall through to JSON output for programmatic callers
588
+ }
589
+
590
+ // Manual / round mode — JSON output (backward compat)
344
591
  if (countLines(diff) < MIN_DIFF_LINES) {
345
592
  exit({ review: 'No significant changes to review' });
346
593
  }