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.
@@ -3,12 +3,21 @@
3
3
  * dual-brain-think.mjs
4
4
  *
5
5
  * Runs a dual-perspective thinking process — GPT-5.5 (via Codex CLI) independently
6
- * analyzes a question, then emits its output along with instructions for Claude
7
- * (the main session) to provide its own independent analysis and compare both.
6
+ * analyzes a question, then Claude provides its own independent analysis, and both
7
+ * perspectives are synthesized into a final recommendation.
8
8
  *
9
- * Usage as CLI:
10
- * node .claude/hooks/dual-brain-think.mjs \
11
- * --question "Should we use queues or direct API calls for the notification system?"
9
+ * Auto mode (default — no --round flag):
10
+ * Runs the full 2-round collaboration automatically in one command.
11
+ * node .claude/hooks/dual-brain-think.mjs --question "Should we use Redis?"
12
+ *
13
+ * Manual Round 1:
14
+ * node .claude/hooks/dual-brain-think.mjs --question "..." --round 1
15
+ *
16
+ * Manual Round 2:
17
+ * node .claude/hooks/dual-brain-think.mjs --question "..." --round 2 --claude-says "<analysis>"
18
+ *
19
+ * Force manual mode (skip auto):
20
+ * node .claude/hooks/dual-brain-think.mjs --question "..." --manual
12
21
  *
13
22
  * Usage as module:
14
23
  * import { dualThink } from './dual-brain-think.mjs';
@@ -27,6 +36,7 @@ import { fileURLToPath } from 'url';
27
36
  const __dirname = dirname(fileURLToPath(import.meta.url));
28
37
 
29
38
  const CODEX_TIMEOUT_MS = 120_000;
39
+ const CLAUDE_TIMEOUT_MS = 60_000;
30
40
  const MODEL = 'gpt-5.5';
31
41
 
32
42
  // ---------------------------------------------------------------------------
@@ -56,6 +66,30 @@ function findCodex() {
56
66
  return null;
57
67
  }
58
68
 
69
+ // ---------------------------------------------------------------------------
70
+ // Claude CLI discovery
71
+ // ---------------------------------------------------------------------------
72
+
73
+ function findClaude() {
74
+ try {
75
+ const which = spawnSync('which', ['claude'], { encoding: 'utf8', stdio: 'pipe', timeout: 3000 });
76
+ if (which.status === 0 && which.stdout.trim()) return which.stdout.trim();
77
+ } catch {}
78
+ const home = process.env.HOME || process.env.USERPROFILE || '';
79
+ const fallbacks = [
80
+ join(home, '.local', 'bin', 'claude'),
81
+ join(home, 'bin', 'claude'),
82
+ '/usr/local/bin/claude',
83
+ ];
84
+ for (const p of fallbacks) {
85
+ try {
86
+ const res = spawnSync(p, ['--version'], { stdio: 'pipe', timeout: 3000 });
87
+ if (res.status === 0) return p;
88
+ } catch {}
89
+ }
90
+ return null;
91
+ }
92
+
59
93
  // ---------------------------------------------------------------------------
60
94
  // Prompt builder
61
95
  // ---------------------------------------------------------------------------
@@ -161,6 +195,102 @@ function runGptAnalysis(codexBin, prompt) {
161
195
  };
162
196
  }
163
197
 
198
+ // ---------------------------------------------------------------------------
199
+ // Claude CLI executor
200
+ // ---------------------------------------------------------------------------
201
+
202
+ function runClaudeAnalysis(claudeBin, question, context) {
203
+ const prompt = `You are providing an independent analysis for a dual-brain architecture discussion. Question: ${question}${context ? `\n\nContext: ${context}` : ''}
204
+
205
+ Provide:
206
+ 1) Your recommendation (clear, 1-2 sentences)
207
+ 2) Key alternatives considered
208
+ 3) Risks with your recommendation
209
+ 4) Verification approach
210
+
211
+ Be concise — under 300 words.`;
212
+
213
+ const startTime = Date.now();
214
+
215
+ const proc = spawnSync(claudeBin, ['-p', prompt], {
216
+ encoding: 'utf8',
217
+ stdio: ['pipe', 'pipe', 'pipe'],
218
+ timeout: CLAUDE_TIMEOUT_MS,
219
+ });
220
+
221
+ const durationMs = Date.now() - startTime;
222
+
223
+ if (proc.status === 0 && proc.stdout && proc.stdout.trim()) {
224
+ return {
225
+ success: true,
226
+ text: proc.stdout.trim(),
227
+ durationMs,
228
+ };
229
+ }
230
+
231
+ return {
232
+ success: false,
233
+ error: proc.stderr?.slice(0, 200) || 'Claude CLI returned no output',
234
+ durationMs,
235
+ };
236
+ }
237
+
238
+ // ---------------------------------------------------------------------------
239
+ // Synthesis builder — pattern-based, no AI call
240
+ // ---------------------------------------------------------------------------
241
+
242
+ function buildSynthesis(gptR1Text, claudeText, gptR2Text) {
243
+ const lines = [];
244
+
245
+ lines.push('SYNTHESIS');
246
+ lines.push('─'.repeat(50));
247
+
248
+ // Extract agreements from GPT Round 2 AGREEMENTS section
249
+ const agreementsMatch = gptR2Text.match(/AGREEMENTS?[:\s\n]+([\s\S]*?)(?=\n\s*(?:PUSHBACK|NEW INSIGHTS|REFINED|REMAINING|CONFIDENCE|[0-9]+\.)|$)/i);
250
+ if (agreementsMatch) {
251
+ lines.push('');
252
+ lines.push('AGREEMENTS (both aligned):');
253
+ lines.push(agreementsMatch[1].trim().split('\n').slice(0, 4).join('\n'));
254
+ }
255
+
256
+ // Extract pushback / disagreements from GPT Round 2
257
+ const pushbackMatch = gptR2Text.match(/PUSHBACK[:\s\n]+([\s\S]*?)(?=\n\s*(?:NEW INSIGHTS|REFINED|REMAINING|CONFIDENCE|[0-9]+\.)|$)/i);
258
+ if (pushbackMatch && pushbackMatch[1].trim().length > 10) {
259
+ lines.push('');
260
+ lines.push('DISAGREEMENTS (review carefully):');
261
+ lines.push(pushbackMatch[1].trim().split('\n').slice(0, 4).join('\n'));
262
+ }
263
+
264
+ // Extract refined recommendation from GPT Round 2
265
+ const refinedMatch = gptR2Text.match(/REFINED RECOMMENDATION[:\s\n]+([\s\S]*?)(?=\n\s*(?:REMAINING|CONFIDENCE|[0-9]+\.)|$)/i);
266
+ if (refinedMatch) {
267
+ lines.push('');
268
+ lines.push('RECOMMENDED ACTION:');
269
+ lines.push(refinedMatch[1].trim().split('\n').slice(0, 3).join('\n'));
270
+ } else {
271
+ // Fall back to R1 recommendation
272
+ const r1RecMatch = gptR1Text.match(/RECOMMENDATION[:\s\n]+([\s\S]*?)(?=\n\s*(?:RATIONALE|ALTERNATIVES|RISKS|CONFIDENCE|[0-9]+\.)|$)/i);
273
+ if (r1RecMatch) {
274
+ lines.push('');
275
+ lines.push('RECOMMENDED ACTION (from Round 1):');
276
+ lines.push(r1RecMatch[1].trim().split('\n').slice(0, 2).join('\n'));
277
+ }
278
+ }
279
+
280
+ // Confidence note
281
+ const confDeltaMatch = gptR2Text.match(/CONFIDENCE DELTA[:\s\n]+([\s\S]*?)(?=\n\s*[0-9]+\.|$)/i);
282
+ if (confDeltaMatch) {
283
+ lines.push('');
284
+ lines.push('CONFIDENCE NOTE:');
285
+ lines.push(confDeltaMatch[1].trim().split('\n').slice(0, 2).join('\n'));
286
+ }
287
+
288
+ lines.push('');
289
+ lines.push('─'.repeat(50));
290
+
291
+ return lines.join('\n');
292
+ }
293
+
164
294
  // ---------------------------------------------------------------------------
165
295
  // Usage logger — matches schema_version: 2 used across the orchestrator
166
296
  // ---------------------------------------------------------------------------
@@ -276,6 +406,131 @@ export async function dualThink({ question, context, files, round, claudePerspec
276
406
  };
277
407
  }
278
408
 
409
+ // ---------------------------------------------------------------------------
410
+ // Auto mode — full 2-round collaboration in one command
411
+ // ---------------------------------------------------------------------------
412
+
413
+ async function runAutoMode({ question, context, files }) {
414
+ const BAR = '╠══════════════════════════════════════════════════╣';
415
+ const TOP = '╔══════════════════════════════════════════════════╗';
416
+ const BOT = '╚══════════════════════════════════════════════════╝';
417
+ const WIDE = '║';
418
+
419
+ const qShort = question.length > 44 ? question.slice(0, 41) + '...' : question;
420
+
421
+ console.log(TOP);
422
+ console.log(`${WIDE} Dual-Brain Think — Auto Mode`.padEnd(51) + WIDE);
423
+ console.log(BAR);
424
+ console.log(`${WIDE} Question: ${qShort.padEnd(38)} ${WIDE}`);
425
+ console.log(BOT);
426
+ console.log('');
427
+
428
+ // Step 1: Check Codex
429
+ const codexBin = findCodex();
430
+ if (!codexBin) {
431
+ console.log('[Auto mode] Codex CLI not found — falling back to manual mode.');
432
+ console.log('');
433
+ console.log('Manual steps:');
434
+ console.log(` 1. Run: node hooks/dual-brain-think.mjs --question "${question}" --round 1`);
435
+ console.log(` 2. Analyze independently`);
436
+ console.log(` 3. Run: node hooks/dual-brain-think.mjs --question "${question}" --round 2 --claude-says "<your analysis>"`);
437
+ return;
438
+ }
439
+
440
+ try {
441
+ execSync(`${codexBin} login status`, {
442
+ encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 5000,
443
+ });
444
+ } catch {
445
+ console.log('[Auto mode] Codex not authenticated (run `codex login`) — falling back to manual mode.');
446
+ console.log('');
447
+ console.log('Manual steps:');
448
+ console.log(` 1. Run: node hooks/dual-brain-think.mjs --question "${question}" --round 1`);
449
+ console.log(` 2. Analyze independently`);
450
+ console.log(` 3. Run: node hooks/dual-brain-think.mjs --question "${question}" --round 2 --claude-says "<your analysis>"`);
451
+ return;
452
+ }
453
+
454
+ // Step 2: Round 1 — GPT analysis
455
+ console.log('[ 1/4 ] Sending to GPT for Round 1 analysis...');
456
+ const r1Prompt = buildGptPrompt({ question, context, files, round: 1 });
457
+ const r1Raw = runGptAnalysis(codexBin, r1Prompt);
458
+ logUsage({ durationMs: r1Raw.durationMs, usage: r1Raw.usage, success: r1Raw.success });
459
+
460
+ if (!r1Raw.success) {
461
+ console.log(`[Auto mode] GPT Round 1 failed: ${r1Raw.error}`);
462
+ console.log('Falling back to manual mode — see instructions above.');
463
+ return;
464
+ }
465
+
466
+ console.log('');
467
+ console.log(TOP);
468
+ console.log(`${WIDE} Round 1 — GPT Analysis (${(r1Raw.durationMs / 1000).toFixed(1)}s)`.padEnd(51) + WIDE);
469
+ console.log(BOT);
470
+ console.log('');
471
+ console.log(r1Raw.text);
472
+ console.log('');
473
+
474
+ // Step 3: Claude's independent analysis
475
+ const claudeBin = findClaude();
476
+ let claudeText = null;
477
+
478
+ if (!claudeBin) {
479
+ console.log('[Auto mode] Claude CLI not found — skipping Claude analysis step.');
480
+ console.log('Set your PATH to include the `claude` binary to enable full auto mode.');
481
+ console.log('');
482
+ } else {
483
+ console.log('[ 2/4 ] Generating Claude independent analysis...');
484
+ const claudeRaw = runClaudeAnalysis(claudeBin, question, context);
485
+
486
+ if (!claudeRaw.success) {
487
+ console.log(`[Auto mode] Claude analysis failed: ${claudeRaw.error}`);
488
+ console.log('Continuing with GPT Round 2 without Claude perspective.');
489
+ console.log('');
490
+ } else {
491
+ claudeText = claudeRaw.text;
492
+ console.log('');
493
+ console.log(TOP);
494
+ console.log(`${WIDE} Claude Independent Analysis (${(claudeRaw.durationMs / 1000).toFixed(1)}s)`.padEnd(51) + WIDE);
495
+ console.log(BOT);
496
+ console.log('');
497
+ console.log(claudeText);
498
+ console.log('');
499
+ }
500
+ }
501
+
502
+ // Step 4: Round 2 — GPT rebuttal
503
+ const claudePerspective = claudeText || '(Claude analysis unavailable — review independently)';
504
+ console.log('[ 3/4 ] Sending Round 2 to GPT with Claude perspective...');
505
+ const r2Prompt = buildGptPrompt({ question, context, files, round: 2, claudePerspective });
506
+ const r2Raw = runGptAnalysis(codexBin, r2Prompt);
507
+ logUsage({ durationMs: r2Raw.durationMs, usage: r2Raw.usage, success: r2Raw.success });
508
+
509
+ if (!r2Raw.success) {
510
+ console.log(`[Auto mode] GPT Round 2 failed: ${r2Raw.error}`);
511
+ console.log('Synthesis skipped — review Round 1 and Claude analysis above.');
512
+ return;
513
+ }
514
+
515
+ console.log('');
516
+ console.log(TOP);
517
+ console.log(`${WIDE} Round 2 — GPT Rebuttal (${(r2Raw.durationMs / 1000).toFixed(1)}s)`.padEnd(51) + WIDE);
518
+ console.log(BOT);
519
+ console.log('');
520
+ console.log(r2Raw.text);
521
+ console.log('');
522
+
523
+ // Step 5: Synthesis
524
+ console.log('[ 4/4 ] Building synthesis...');
525
+ console.log('');
526
+ console.log(TOP);
527
+ console.log(`${WIDE} Final Synthesis`.padEnd(51) + WIDE);
528
+ console.log(BOT);
529
+ console.log('');
530
+ console.log(buildSynthesis(r1Raw.text, claudeText || '', r2Raw.text));
531
+ console.log('');
532
+ }
533
+
279
534
  // ---------------------------------------------------------------------------
280
535
  // CLI argument parser
281
536
  // ---------------------------------------------------------------------------
@@ -312,7 +567,7 @@ function parseArgs(argv) {
312
567
  }
313
568
 
314
569
  // ---------------------------------------------------------------------------
315
- // CLI output formatter
570
+ // CLI output formatter (manual mode)
316
571
  // ---------------------------------------------------------------------------
317
572
 
318
573
  function printResult(result, question) {
@@ -323,23 +578,23 @@ function printResult(result, question) {
323
578
  const roundLabel = result.round === 2 ? 'Round 2 — Rebuttal' : 'Round 1 — Initial';
324
579
 
325
580
  console.log(TOP);
326
- console.log(`║ 🧠 Dual-Brain Think · ${roundLabel}`.padEnd(51) + '║');
581
+ console.log(`║ Dual-Brain Think · ${roundLabel}`.padEnd(51) + '║');
327
582
  console.log(BAR);
328
583
  const q = question.length > 44 ? question.slice(0, 41) + '...' : question;
329
584
  console.log(`║ Question: ${q.padEnd(38)} ║`);
330
585
  console.log(BAR);
331
586
 
332
587
  if (!result.gpt) {
333
- console.log(`║${(result.error || 'Unknown error').padEnd(45)} ║`);
588
+ console.log(`║ ${(result.error || 'Unknown error').padEnd(46)} ║`);
334
589
  console.log(BAR);
335
- console.log(`║ ↩️ ${(result.fallback || '').padEnd(45)} ║`);
590
+ console.log(`║ ${(result.fallback || '').padEnd(46)} ║`);
336
591
  console.log(BOT);
337
592
  return;
338
593
  }
339
594
 
340
595
  const gptData = result.gpt;
341
596
  const durSec = (gptData.durationMs / 1000).toFixed(1);
342
- console.log(`║ 🤖 GPT-5.5 (${durSec}s):`.padEnd(51) + '║');
597
+ console.log(`║ GPT-5.5 (${durSec}s):`.padEnd(51) + '║');
343
598
  console.log(BAR);
344
599
  console.log('');
345
600
  console.log(gptData.recommendation || gptData.rebuttal);
@@ -347,13 +602,13 @@ function printResult(result, question) {
347
602
  console.log(BAR);
348
603
 
349
604
  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.║');
605
+ console.log('║ Synthesize both rounds into final decision. ║');
606
+ console.log('║ Where you agree → high confidence. ║');
607
+ console.log('║ Where you disagree → state what would resolve. ║');
353
608
  } 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. ║');
609
+ console.log('║ Your turn: analyze independently, then call ║');
610
+ console.log('║ Round 2 with --round 2 --claude-says "..." ║');
611
+ console.log('║ for GPT\'s rebuttal to your analysis. ║');
357
612
  }
358
613
  console.log(BOT);
359
614
  }
@@ -368,18 +623,32 @@ if (import.meta.url === `file://${process.argv[1]}`) {
368
623
  if (!args.question) {
369
624
  console.error(
370
625
  '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>"'
626
+ ' node dual-brain-think.mjs --question "<question>" --round 2 --claude-says "<analysis>"\n' +
627
+ ' node dual-brain-think.mjs --question "<question>" --manual (force old 1-step flow)'
372
628
  );
373
629
  process.exit(1);
374
630
  }
375
631
 
376
- const result = await dualThink({
377
- question: args.question,
378
- context: args.context,
379
- files: args.files,
380
- round: args.round ? parseInt(args.round, 10) : 1,
381
- claudePerspective: args['claude-says'] || null,
382
- });
632
+ const hasExplicitRound = args.round !== undefined;
633
+ const isManual = args.manual === true || hasExplicitRound;
383
634
 
384
- printResult(result, args.question);
635
+ if (!isManual) {
636
+ // Auto mode: full 2-round collaboration in one shot
637
+ await runAutoMode({
638
+ question: args.question,
639
+ context: args.context,
640
+ files: args.files,
641
+ });
642
+ } else {
643
+ // Manual mode: original single-round behavior
644
+ const result = await dualThink({
645
+ question: args.question,
646
+ context: args.context,
647
+ files: args.files,
648
+ round: args.round ? parseInt(args.round, 10) : 1,
649
+ claudePerspective: args['claude-says'] || null,
650
+ });
651
+
652
+ printResult(result, args.question);
653
+ }
385
654
  }